Merge branch 'main' into oleina/logging

This commit is contained in:
Ola Hungerford 2025-07-26 09:43:20 -07:00 • committed by GitHub
commit 1ed920297f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -71,6 +71,10 @@ import {
initializeInspectorConfig,
saveInspectorConfig,
} from "./utils/configUtils";
import ElicitationTab, {
PendingElicitationRequest,
ElicitationResponse,
} from "./components/ElicitationTab";
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
@ -119,6 +123,14 @@ const App = () => {
return localStorage.getItem("lastHeaderName") || "";
});
const [oauthClientId, setOauthClientId] = useState<string>(() => {
return localStorage.getItem("lastOauthClientId") || "";
});
const [oauthScope, setOauthScope] = useState<string>(() => {
return localStorage.getItem("lastOauthScope") || "";
});
const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array<
PendingRequest & {
@ -127,13 +139,19 @@ const App = () => {
}
>
>([]);
const [pendingElicitationRequests, setPendingElicitationRequests] = useState<
Array<
PendingElicitationRequest & {
resolve: (response: ElicitationResponse) => void;
decline: (error: Error) => void;
}
>
>([]);
const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);
// Auth debugger state
const [authState, setAuthState] =
useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);
// Helper function to update specific auth state properties
const updateAuthState = (updates: Partial<AuthDebuggerState>) => {
setAuthState((prev) => ({ ...prev, ...updates }));
};
@ -161,6 +179,19 @@ const App = () => {
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
const progressTokenRef = useRef(0);
const [activeTab, setActiveTab] = useState<string>(() => {
const hash = window.location.hash.slice(1);
const initialTab = hash || "resources";
return initialTab;
});
const currentTabRef = useRef<string>(activeTab);
const lastToolCallOriginTabRef = useRef<string>(activeTab);
useEffect(() => {
currentTabRef.current = activeTab;
}, [activeTab]);
const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
const {
width: sidebarWidth,
@ -187,6 +218,8 @@ const App = () => {
env,
bearerToken,
headerName,
oauthClientId,
oauthScope,
config,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
@ -203,10 +236,65 @@ const App = () => {
{ id: nextRequestId.current++, request, resolve, reject },
]);
},
onElicitationRequest: (request, resolve) => {
const currentTab = lastToolCallOriginTabRef.current;
setPendingElicitationRequests((prev) => [
...prev,
{
id: nextRequestId.current++,
request: {
id: nextRequestId.current,
message: request.params.message,
requestedSchema: request.params.requestedSchema,
},
originatingTab: currentTab,
resolve,
decline: (error: Error) => {
console.error("Elicitation request rejected:", error);
},
},
]);
setActiveTab("elicitations");
window.location.hash = "elicitations";
},
getRoots: () => rootsRef.current,
defaultLoggingLevel: logLevel,
});
useEffect(() => {
if (serverCapabilities) {
const hash = window.location.hash.slice(1);
const validTabs = [
...(serverCapabilities?.resources ? ["resources"] : []),
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
"ping",
"sampling",
"elicitations",
"roots",
"auth",
];
const isValidTab = validTabs.includes(hash);
if (!isValidTab) {
const defaultTab = serverCapabilities?.resources
? "resources"
: serverCapabilities?.prompts
? "prompts"
: serverCapabilities?.tools
? "tools"
: "ping";
setActiveTab(defaultTab);
window.location.hash = defaultTab;
}
}
}, [serverCapabilities]);
useEffect(() => {
localStorage.setItem("lastCommand", command);
}, [command]);
@ -231,11 +319,18 @@ const App = () => {
localStorage.setItem("lastHeaderName", headerName);
}, [headerName]);
useEffect(() => {
localStorage.setItem("lastOauthClientId", oauthClientId);
}, [oauthClientId]);
useEffect(() => {
localStorage.setItem("lastOauthScope", oauthScope);
}, [oauthScope]);
useEffect(() => {
saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
}, [config]);
// Auto-connect to previously saved serverURL after OAuth callback
const onOAuthConnect = useCallback(
(serverUrl: string) => {
setSseUrl(serverUrl);
@ -245,7 +340,6 @@ const App = () => {
[connectMcpServer],
);
// Update OAuth debug state during debug callback
const onOAuthDebugConnect = useCallback(
async ({
authorizationCode,
@ -266,7 +360,6 @@ const App = () => {
}
if (restoredState && authorizationCode) {
// Restore the previous auth state and continue the OAuth flow
let currentState: AuthDebuggerState = {
...restoredState,
authorizationCode,
@ -277,12 +370,10 @@ const App = () => {
};
try {
// Create a new state machine instance to continue the flow
const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {
currentState = { ...currentState, ...updates };
});
// Continue stepping through the OAuth flow from where we left off
while (
currentState.oauthStep !== "complete" &&
currentState.oauthStep !== "authorization_code"
@ -291,7 +382,6 @@ const App = () => {
}
if (currentState.oauthStep === "complete") {
// After the flow completes or reaches a user-input step, update the app state
updateAuthState({
...currentState,
statusMessage: {
@ -314,7 +404,6 @@ const App = () => {
});
}
} else if (authorizationCode) {
// Fallback to the original behavior if no state was restored
updateAuthState({
authorizationCode,
oauthStep: "token_request",
@ -324,7 +413,6 @@ const App = () => {
[sseUrl],
);
// Load OAuth tokens when sseUrl changes
useEffect(() => {
const loadOAuthTokens = async () => {
try {
@ -383,6 +471,18 @@ const App = () => {
}
}, []);
useEffect(() => {
const handleHashChange = () => {
const hash = window.location.hash.slice(1);
if (hash && hash !== activeTab) {
setActiveTab(hash);
}
};
window.addEventListener("hashchange", handleHashChange);
return () => window.removeEventListener("hashchange", handleHashChange);
}, [activeTab]);
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
setPendingSampleRequests((prev) => {
const request = prev.find((r) => r.id === id);
@ -399,6 +499,44 @@ const App = () => {
});
};
const handleResolveElicitation = (
id: number,
response: ElicitationResponse,
) => {
setPendingElicitationRequests((prev) => {
const request = prev.find((r) => r.id === id);
if (request) {
request.resolve(response);
if (request.originatingTab) {
const originatingTab = request.originatingTab;
const validTabs = [
...(serverCapabilities?.resources ? ["resources"] : []),
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
"ping",
"sampling",
"elicitations",
"roots",
"auth",
];
if (validTabs.includes(originatingTab)) {
setActiveTab(originatingTab);
window.location.hash = originatingTab;
setTimeout(() => {
setActiveTab(originatingTab);
window.location.hash = originatingTab;
}, 100);
}
}
}
return prev.filter((r) => r.id !== id);
});
};
const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null }));
};
@ -456,7 +594,23 @@ const App = () => {
setNextResourceTemplateCursor(response.nextCursor);
};
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
lastToolCallOriginTabRef.current = currentTabRef.current;
const response = await sendMCPRequest(
{
method: "prompts/get" as const,
params: { name, arguments: args },
},
GetPromptResultSchema,
"prompts",
);
setPromptContent(JSON.stringify(response, null, 2));
};
const readResource = async (uri: string) => {
lastToolCallOriginTabRef.current = currentTabRef.current;
const response = await sendMCPRequest(
{
method: "resources/read" as const,
@ -518,18 +672,6 @@ const App = () => {
setNextPromptCursor(response.nextCursor);
};
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
const response = await sendMCPRequest(
{
method: "prompts/get" as const,
params: { name, arguments: args },
},
GetPromptResultSchema,
"prompts",
);
setPromptContent(JSON.stringify(response, null, 2));
};
const listTools = async () => {
const response = await sendMCPRequest(
{
@ -541,11 +683,12 @@ const App = () => {
);
setTools(response.tools);
setNextToolCursor(response.nextCursor);
// Cache output schemas for validation
cacheToolOutputSchemas(response.tools);
};
const callTool = async (name: string, params: Record<string, unknown>) => {
lastToolCallOriginTabRef.current = currentTabRef.current;
try {
const response = await sendMCPRequest(
{
@ -561,6 +704,7 @@ const App = () => {
CompatibilityCallToolResultSchema,
"tools",
);
setToolResult(response);
} catch (e) {
const toolResult: CompatibilityCallToolResult = {
@ -595,7 +739,6 @@ const App = () => {
setStdErrNotifications([]);
};
// Helper component for rendering the AuthDebugger
const AuthDebuggerWrapper = () => (
<TabsContent value="auth">
<AuthDebugger
@ -607,7 +750,6 @@ const App = () => {
</TabsContent>
);
// Helper function to render OAuth callback components
if (window.location.pathname === "/oauth/callback") {
const OAuthCallback = React.lazy(
() => import("./components/OAuthCallback"),
@ -659,6 +801,10 @@ const App = () => {
setBearerToken={setBearerToken}
headerName={headerName}
setHeaderName={setHeaderName}
oauthClientId={oauthClientId}
setOauthClientId={setOauthClientId}
oauthScope={oauthScope}
setOauthScope={setOauthScope}
onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer}
stdErrNotifications={stdErrNotifications}
@ -667,7 +813,6 @@ const App = () => {
loggingSupported={!!serverCapabilities?.logging || false}
clearStdErrNotifications={clearStdErrNotifications}
/>
{/* Drag handle for resizing sidebar */}
<div
onMouseDown={handleSidebarDragStart}
style={{
@ -688,21 +833,12 @@ const App = () => {
<div className="flex-1 overflow-auto">
{mcpClient ? (
<Tabs
defaultValue={
Object.keys(serverCapabilities ?? {}).includes(
window.location.hash.slice(1),
)
? window.location.hash.slice(1)
: serverCapabilities?.resources
? "resources"
: serverCapabilities?.prompts
? "prompts"
: serverCapabilities?.tools
? "tools"
: "ping"
}
value={activeTab}
className="w-full p-4"
onValueChange={(value) => (window.location.hash = value)}
onValueChange={(value) => {
setActiveTab(value);
window.location.hash = value;
}}
>
<TabsList className="mb-4 py-0">
<TabsTrigger
@ -739,6 +875,15 @@ const App = () => {
</span>
)}
</TabsTrigger>
<TabsTrigger value="elicitations" className="relative">
<MessageSquare className="w-4 h-4 mr-2" />
Elicitations
{pendingElicitationRequests.length > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
{pendingElicitationRequests.length}
</span>
)}
</TabsTrigger>
<TabsTrigger value="roots">
<FolderTree className="w-4 h-4 mr-2" />
Roots
@ -855,7 +1000,6 @@ const App = () => {
clearTools={() => {
setTools([]);
setNextToolCursor(undefined);
// Clear cached output schemas
cacheToolOutputSchemas([]);
}}
callTool={async (name, params) => {
@ -894,6 +1038,10 @@ const App = () => {
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
<ElicitationTab
pendingRequests={pendingElicitationRequests}
onResolve={handleResolveElicitation}
/>
<RootsTab
roots={roots}
setRoots={setRoots}