Skip to content
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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,33 @@ You can paste the Server Entry into your existing `mcp.json` file under your cho

### Authentication

The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name using the input field in the sidebar.
The inspector supports multiple authentication methods for SSE and Streamable HTTP connections:

#### Bearer Token

Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name using the input field in the sidebar.

#### OAuth 2.0 with Client Secret

Select **Client Secret** as the authentication method in the OAuth section. Provide your Client ID, optional Client Secret, and Scope. The inspector handles the full OAuth 2.0 Authorization Code flow including PKCE.

#### OAuth 2.0 with Client Certificate

Select **Client Certificate** as the authentication method for certificate-based OAuth (`private_key_jwt`). This uses a signed JWT assertion backed by an X.509 certificate instead of a client secret.

**Required fields:**

- **Client ID** — Your OAuth application's client ID
- **Certificate File Path** — Path to the X.509 certificate PEM file on the server
- **Private Key File Path** — Path to the RSA private key PEM file on the server
- **Scope** — OAuth scopes (space-separated)

**OAuth endpoint discovery:**

- **Auto-discover from metadata** (default) — The inspector fetches the MCP server's protected resource metadata (`.well-known/oauth-protected-resource`) and resolves authorization/token endpoints via OIDC discovery (`.well-known/openid-configuration`). This works with any OAuth provider that publishes standard metadata.
- **Enter manually** — Provide the Authorization Endpoint URL and Token Endpoint URL directly.

Certificate auth works with any OAuth 2.0 provider supporting `private_key_jwt` client authentication (RFC 7523), including Azure AD/Entra ID, Okta, Auth0, and Keycloak. The JWT assertion includes both `x5t` (thumbprint) and `x5c` (certificate chain) claims for broad compatibility.

### Security Considerations

Expand Down
114 changes: 108 additions & 6 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ import {
isReservedMetaKey,
} from "@/utils/metaUtils";
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
import { OAuthStateMachine } from "./lib/oauth-state-machine";
import {
AuthDebuggerState,
EMPTY_DEBUGGER_STATE,
OAuthClientAuthMethod,
} from "./lib/auth-types";
import { OAuthStateMachine, CertAuthConfig } from "./lib/oauth-state-machine";
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
import { cleanParams } from "./utils/paramUtils";
import type { JsonSchemaType } from "./utils/jsonUtils";
Expand Down Expand Up @@ -210,6 +214,32 @@ const App = () => {
return localStorage.getItem("lastOauthClientSecret") || "";
});

const [oauthAuthMethod, setOauthAuthMethod] = useState<OAuthClientAuthMethod>(
() => {
return (
(localStorage.getItem(
"lastOauthAuthMethod",
) as OAuthClientAuthMethod) || "secret"
);
},
);

const [oauthCertPath, setOauthCertPath] = useState<string>(() => {
return localStorage.getItem("lastOauthCertPath") || "";
});

const [oauthKeyPath, setOauthKeyPath] = useState<string>(() => {
return localStorage.getItem("lastOauthKeyPath") || "";
});

const [oauthTokenEndpoint, setOauthTokenEndpoint] = useState<string>(() => {
return localStorage.getItem("lastOauthTokenEndpoint") || "";
});

const [oauthAuthEndpoint, setOauthAuthEndpoint] = useState<string>(() => {
return localStorage.getItem("lastOauthAuthEndpoint") || "";
});

// Custom headers state with migration from legacy auth
const [customHeaders, setCustomHeaders] = useState<CustomHeaders>(() => {
const savedHeaders = localStorage.getItem("lastCustomHeaders");
Expand Down Expand Up @@ -395,6 +425,11 @@ const App = () => {
oauthClientId,
oauthClientSecret,
oauthScope,
oauthAuthMethod,
oauthCertPath,
oauthKeyPath,
oauthTokenEndpoint,
oauthAuthEndpoint,
config,
connectionType,
onNotification: (notification) => {
Expand Down Expand Up @@ -579,6 +614,26 @@ const App = () => {
localStorage.setItem("lastOauthClientSecret", oauthClientSecret);
}, [oauthClientSecret]);

useEffect(() => {
localStorage.setItem("lastOauthAuthMethod", oauthAuthMethod);
}, [oauthAuthMethod]);

useEffect(() => {
localStorage.setItem("lastOauthCertPath", oauthCertPath);
}, [oauthCertPath]);

useEffect(() => {
localStorage.setItem("lastOauthKeyPath", oauthKeyPath);
}, [oauthKeyPath]);

useEffect(() => {
localStorage.setItem("lastOauthTokenEndpoint", oauthTokenEndpoint);
}, [oauthTokenEndpoint]);

useEffect(() => {
localStorage.setItem("lastOauthAuthEndpoint", oauthAuthEndpoint);
}, [oauthAuthEndpoint]);

useEffect(() => {
saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
}, [config]);
Expand Down Expand Up @@ -622,9 +677,30 @@ const App = () => {
};

try {
const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {
currentState = { ...currentState, ...updates };
});
const certConfig: CertAuthConfig | undefined =
oauthAuthMethod === "certificate"
? {
authMethod: oauthAuthMethod,
certPath: oauthCertPath,
keyPath: oauthKeyPath,
tokenEndpointUrl: oauthTokenEndpoint,
authEndpointUrl: oauthAuthEndpoint,
}
: undefined;
const proxyAddress = getMCPProxyAddress(config);
const { token: proxyToken, header: proxyHeader } =
getMCPProxyAuthToken(config);

const stateMachine = new OAuthStateMachine(
sseUrl,
(updates) => {
currentState = { ...currentState, ...updates };
},
certConfig,
proxyAddress,
proxyToken,
proxyHeader,
);

while (
currentState.oauthStep !== "complete" &&
Expand Down Expand Up @@ -662,7 +738,15 @@ const App = () => {
});
}
},
[sseUrl],
[
sseUrl,
config,
oauthAuthMethod,
oauthCertPath,
oauthKeyPath,
oauthTokenEndpoint,
oauthAuthEndpoint,
],
);

useEffect(() => {
Expand Down Expand Up @@ -1264,6 +1348,14 @@ const App = () => {
onBack={() => setIsAuthDebuggerVisible(false)}
authState={authState}
updateAuthState={updateAuthState}
oauthAuthMethod={oauthAuthMethod}
oauthCertPath={oauthCertPath}
oauthKeyPath={oauthKeyPath}
oauthTokenEndpoint={oauthTokenEndpoint}
oauthAuthEndpoint={oauthAuthEndpoint}
proxyAddress={getMCPProxyAddress(config)}
proxyAuthToken={getMCPProxyAuthToken(config).token}
proxyAuthHeader={getMCPProxyAuthToken(config).header}
/>
</TabsContent>
);
Expand Down Expand Up @@ -1323,6 +1415,16 @@ const App = () => {
setOauthClientSecret={setOauthClientSecret}
oauthScope={oauthScope}
setOauthScope={setOauthScope}
oauthAuthMethod={oauthAuthMethod}
setOauthAuthMethod={setOauthAuthMethod}
oauthCertPath={oauthCertPath}
setOauthCertPath={setOauthCertPath}
oauthKeyPath={oauthKeyPath}
setOauthKeyPath={setOauthKeyPath}
oauthTokenEndpoint={oauthTokenEndpoint}
setOauthTokenEndpoint={setOauthTokenEndpoint}
oauthAuthEndpoint={oauthAuthEndpoint}
setOauthAuthEndpoint={setOauthAuthEndpoint}
onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer}
logLevel={logLevel}
Expand Down
90 changes: 80 additions & 10 deletions client/src/components/AuthDebugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { useCallback, useMemo, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { DebugInspectorOAuthClientProvider } from "../lib/auth";
import { AlertCircle } from "lucide-react";
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types";
import {
AuthDebuggerState,
EMPTY_DEBUGGER_STATE,
OAuthClientAuthMethod,
} from "../lib/auth-types";
import { OAuthFlowProgress } from "./OAuthFlowProgress";
import { OAuthStateMachine } from "../lib/oauth-state-machine";
import { OAuthStateMachine, CertAuthConfig } from "../lib/oauth-state-machine";
import { SESSION_KEYS } from "../lib/constants";
import { validateRedirectUrl } from "@/utils/urlValidation";

Expand All @@ -13,6 +17,14 @@ export interface AuthDebuggerProps {
onBack: () => void;
authState: AuthDebuggerState;
updateAuthState: (updates: Partial<AuthDebuggerState>) => void;
oauthAuthMethod?: OAuthClientAuthMethod;
oauthCertPath?: string;
oauthKeyPath?: string;
oauthTokenEndpoint?: string;
oauthAuthEndpoint?: string;
proxyAddress?: string;
proxyAuthToken?: string;
proxyAuthHeader?: string;
}

interface StatusMessageProps {
Expand Down Expand Up @@ -60,6 +72,14 @@ const AuthDebugger = ({
onBack,
authState,
updateAuthState,
oauthAuthMethod,
oauthCertPath,
oauthKeyPath,
oauthTokenEndpoint,
oauthAuthEndpoint,
proxyAddress,
proxyAuthToken,
proxyAuthHeader,
}: AuthDebuggerProps) => {
// Check for existing tokens on mount
useEffect(() => {
Expand Down Expand Up @@ -102,9 +122,44 @@ const AuthDebugger = ({
});
}, [serverUrl, updateAuthState]);

const certConfig: CertAuthConfig | undefined = useMemo(
() =>
oauthAuthMethod === "certificate"
? {
authMethod: oauthAuthMethod,
certPath: oauthCertPath || "",
keyPath: oauthKeyPath || "",
tokenEndpointUrl: oauthTokenEndpoint || "",
authEndpointUrl: oauthAuthEndpoint || "",
}
: undefined,
[
oauthAuthMethod,
oauthCertPath,
oauthKeyPath,
oauthTokenEndpoint,
oauthAuthEndpoint,
],
);

const stateMachine = useMemo(
() => new OAuthStateMachine(serverUrl, updateAuthState),
[serverUrl, updateAuthState],
() =>
new OAuthStateMachine(
serverUrl,
updateAuthState,
certConfig,
proxyAddress,
proxyAuthToken,
proxyAuthHeader,
),
[
serverUrl,
updateAuthState,
certConfig,
proxyAddress,
proxyAuthToken,
proxyAuthHeader,
],
);

const proceedToNextStep = useCallback(async () => {
Expand Down Expand Up @@ -150,11 +205,18 @@ const AuthDebugger = ({
latestError: null,
};

const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => {
// Update our temporary state during the process
currentState = { ...currentState, ...updates };
// But don't call updateAuthState yet
});
const oauthMachine = new OAuthStateMachine(
serverUrl,
(updates) => {
// Update our temporary state during the process
currentState = { ...currentState, ...updates };
// But don't call updateAuthState yet
},
certConfig,
proxyAddress,
proxyAuthToken,
proxyAuthHeader,
);

// Manually step through each stage of the OAuth flow
while (currentState.oauthStep !== "complete") {
Expand Down Expand Up @@ -214,7 +276,15 @@ const AuthDebugger = ({
} finally {
updateAuthState({ isInitiatingAuth: false });
}
}, [serverUrl, updateAuthState, authState]);
}, [
serverUrl,
updateAuthState,
authState,
certConfig,
proxyAddress,
proxyAuthToken,
proxyAuthHeader,
]);

const handleClearOAuth = useCallback(() => {
if (serverUrl) {
Expand Down
Loading