Merge branch 'main' into oleina/logging
This commit is contained in:
commit
1ed920297f
1 changed files with 2048 additions and 178 deletions
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue