Skip to content

feat: support manual entry of OAuth client information #345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ const App = () => {
return localStorage.getItem("lastHeaderName") || "";
});

const [oauthClientId, setOauthClientId] = useState<string>(() => {
return localStorage.getItem("lastOauthClientId") || "";
});

const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array<
PendingRequest & {
Expand Down Expand Up @@ -181,6 +185,7 @@ const App = () => {
env,
bearerToken,
headerName,
oauthClientId,
config,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
Expand Down Expand Up @@ -224,6 +229,10 @@ const App = () => {
localStorage.setItem("lastHeaderName", headerName);
}, [headerName]);

useEffect(() => {
localStorage.setItem("lastOauthClientId", oauthClientId);
}, [oauthClientId]);

useEffect(() => {
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
}, [config]);
Expand Down Expand Up @@ -502,6 +511,8 @@ const App = () => {
setBearerToken={setBearerToken}
headerName={headerName}
setHeaderName={setHeaderName}
oauthClientId={oauthClientId}
setOauthClientId={setOauthClientId}
onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer}
stdErrNotifications={stdErrNotifications}
Expand Down
13 changes: 11 additions & 2 deletions client/src/components/OAuthCallback.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useEffect, useRef } from "react";
import { InspectorOAuthClientProvider } from "../lib/auth";
import {
InspectorOAuthClientProvider,
getClientInformationFromSessionStorage,
} from "../lib/auth";
import { SESSION_KEYS } from "../lib/constants";
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
import { useToast } from "@/hooks/use-toast.ts";
Expand Down Expand Up @@ -41,10 +44,16 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => {
return notifyError("Missing Server URL");
}

const clientInformation =
await getClientInformationFromSessionStorage(serverUrl);

let result;
try {
// Create an auth provider with the current server URL
const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl);
const serverAuthProvider = new InspectorOAuthClientProvider(
serverUrl,
clientInformation,
);

result = await auth(serverAuthProvider, {
serverUrl,
Expand Down
43 changes: 43 additions & 0 deletions client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ interface SidebarProps {
setBearerToken: (token: string) => void;
headerName?: string;
setHeaderName?: (name: string) => void;
oauthClientId: string;
setOauthClientId: (id: string) => void;
onConnect: () => void;
onDisconnect: () => void;
stdErrNotifications: StdErrNotification[];
Expand Down Expand Up @@ -80,6 +82,8 @@ const Sidebar = ({
setBearerToken,
headerName,
setHeaderName,
oauthClientId,
setOauthClientId,
onConnect,
onDisconnect,
stdErrNotifications,
Expand All @@ -95,6 +99,7 @@ const Sidebar = ({
const [showBearerToken, setShowBearerToken] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
const [showOauthConfig, setShowOauthConfig] = useState(false);

return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
Expand Down Expand Up @@ -221,6 +226,44 @@ const Sidebar = ({
</div>
)}
</div>
{/* OAuth Configuration */}
<div className="space-y-2">
<Button
variant="outline"
onClick={() => setShowOauthConfig(!showOauthConfig)}
className="flex items-center w-full"
data-testid="oauth-config-button"
aria-expanded={showOauthConfig}
>
{showOauthConfig ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
OAuth Configuration
</Button>
{showOauthConfig && (
<div className="space-y-2">
<label className="text-sm font-medium">Client ID</label>
<Input
placeholder="Client ID"
onChange={(e) => setOauthClientId(e.target.value)}
value={oauthClientId}
data-testid="oauth-client-id-input"
className="font-mono"
/>
<label className="text-sm font-medium">
Redirect URL (auto-populated)
</label>
<Input
readOnly
placeholder="Redirect URL"
value={window.location.origin + "/oauth/callback"}
className="font-mono"
/>
</div>
)}
</div>
</>
)}
{transportType === "stdio" && (
Expand Down
33 changes: 22 additions & 11 deletions client/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@ import {
} from "@modelcontextprotocol/sdk/shared/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./constants";

export const getClientInformationFromSessionStorage = async (
serverUrl: string,
) => {
const key = getServerSpecificKey(SESSION_KEYS.CLIENT_INFORMATION, serverUrl);
const value = sessionStorage.getItem(key);
if (!value) {
return undefined;
}

return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
};

export class InspectorOAuthClientProvider implements OAuthClientProvider {
constructor(private serverUrl: string) {
constructor(
private serverUrl: string,
clientInformation?: OAuthClientInformation,
) {
// Save the server URL to session storage
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);

// Save the client information to session storage if provided
if (clientInformation) {
this.saveClientInformation(clientInformation);
}
}

get redirectUrl() {
Expand All @@ -29,16 +49,7 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
}

async clientInformation() {
const key = getServerSpecificKey(
SESSION_KEYS.CLIENT_INFORMATION,
this.serverUrl,
);
const value = sessionStorage.getItem(key);
if (!value) {
return undefined;
}

return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
return await getClientInformationFromSessionStorage(this.serverUrl);
}

saveClientInformation(clientInformation: OAuthClientInformation) {
Expand Down
29 changes: 25 additions & 4 deletions client/src/lib/hooks/useConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
Progress,
} from "@modelcontextprotocol/sdk/types.js";
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
import { useState } from "react";
import { useMemo, useState } from "react";
import { useToast } from "@/hooks/use-toast";
import { z } from "zod";
import { ConnectionStatus } from "../constants";
Expand All @@ -40,6 +40,7 @@ import {
} from "@/utils/configUtils";
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
import { InspectorConfig } from "../configurationTypes";
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";

interface UseConnectionOptions {
transportType: "stdio" | "sse" | "streamable-http";
Expand All @@ -49,6 +50,7 @@ interface UseConnectionOptions {
env: Record<string, string>;
bearerToken?: string;
headerName?: string;
oauthClientId?: string;
config: InspectorConfig;
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
Expand All @@ -66,6 +68,7 @@ export function useConnection({
env,
bearerToken,
headerName,
oauthClientId,
config,
onNotification,
onStdErrNotification,
Expand All @@ -83,6 +86,15 @@ export function useConnection({
>([]);
const [completionsSupported, setCompletionsSupported] = useState(true);

const oauthClientInformation: OAuthClientInformation | undefined =
useMemo(() => {
if (!oauthClientId) {
return undefined;
}

return { client_id: oauthClientId };
}, [oauthClientId]);

const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
Expand Down Expand Up @@ -247,7 +259,10 @@ export function useConnection({
const handleAuthError = async (error: unknown) => {
if (error instanceof SseError && error.code === 401) {
// Create a new auth provider with the current server URL
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
const serverAuthProvider = new InspectorOAuthClientProvider(
sseUrl,
oauthClientInformation,
);

const result = await auth(serverAuthProvider, { serverUrl: sseUrl });
return result === "AUTHORIZED";
Expand Down Expand Up @@ -294,7 +309,10 @@ export function useConnection({
const headers: HeadersInit = {};

// Create an auth provider with the current server URL
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
const serverAuthProvider = new InspectorOAuthClientProvider(
sseUrl,
oauthClientInformation,
);

// Use manually provided bearer token if available, otherwise use OAuth tokens
const token =
Expand Down Expand Up @@ -396,7 +414,10 @@ export function useConnection({

const disconnect = async () => {
await mcpClient?.close();
const authProvider = new InspectorOAuthClientProvider(sseUrl);
const authProvider = new InspectorOAuthClientProvider(
sseUrl,
oauthClientInformation,
);
authProvider.clear();
setMcpClient(null);
setConnectionStatus("disconnected");
Expand Down