Skip to content

Commit 826ce37

Browse files
Merge pull request #143 from modelcontextprotocol/justin/sdk-auth
Refactor to use auth from SDK
2 parents 13ae2b5 + 7a56a72 commit 826ce37

File tree

7 files changed

+645
-265
lines changed

7 files changed

+645
-265
lines changed

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"preview": "vite preview"
2222
},
2323
"dependencies": {
24-
"@modelcontextprotocol/sdk": "^1.4.1",
24+
"@modelcontextprotocol/sdk": "^1.6.1",
2525
"@radix-ui/react-dialog": "^1.1.3",
2626
"@radix-ui/react-checkbox": "^1.1.4",
2727
"@radix-ui/react-icons": "^1.3.0",

client/src/components/OAuthCallback.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useRef } from "react";
2-
import { handleOAuthCallback } from "../lib/auth";
2+
import { authProvider } from "../lib/auth";
33
import { SESSION_KEYS } from "../lib/constants";
4+
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
45

56
const OAuthCallback = () => {
67
const hasProcessedRef = useRef(false);
@@ -24,15 +25,16 @@ const OAuthCallback = () => {
2425
}
2526

2627
try {
27-
const tokens = await handleOAuthCallback(serverUrl, code);
28-
// Store both access and refresh tokens
29-
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
30-
if (tokens.refresh_token) {
31-
sessionStorage.setItem(
32-
SESSION_KEYS.REFRESH_TOKEN,
33-
tokens.refresh_token,
28+
const result = await auth(authProvider, {
29+
serverUrl,
30+
authorizationCode: code,
31+
});
32+
if (result !== "AUTHORIZED") {
33+
throw new Error(
34+
`Expected to be authorized after providing auth code, got: ${result}`,
3435
);
3536
}
37+
3638
// Redirect back to the main app with server URL to trigger auto-connect
3739
window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`;
3840
} catch (error) {

client/src/lib/auth.ts

Lines changed: 54 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,73 @@
1-
import pkceChallenge from "pkce-challenge";
1+
import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
2+
import {
3+
OAuthClientInformationSchema,
4+
OAuthClientInformation,
5+
OAuthTokens,
6+
OAuthTokensSchema,
7+
} from "@modelcontextprotocol/sdk/shared/auth.js";
28
import { SESSION_KEYS } from "./constants";
3-
import { z } from "zod";
49

5-
export const OAuthMetadataSchema = z.object({
6-
authorization_endpoint: z.string(),
7-
token_endpoint: z.string(),
8-
});
9-
10-
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
11-
12-
export const OAuthTokensSchema = z.object({
13-
access_token: z.string(),
14-
refresh_token: z.string().optional(),
15-
expires_in: z.number().optional(),
16-
});
17-
18-
export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
19-
20-
export async function discoverOAuthMetadata(
21-
serverUrl: string,
22-
): Promise<OAuthMetadata> {
23-
try {
24-
const url = new URL("/.well-known/oauth-authorization-server", serverUrl);
25-
const response = await fetch(url.toString());
26-
27-
if (response.ok) {
28-
const metadata = await response.json();
29-
const validatedMetadata = OAuthMetadataSchema.parse({
30-
authorization_endpoint: metadata.authorization_endpoint,
31-
token_endpoint: metadata.token_endpoint,
32-
});
33-
return validatedMetadata;
34-
}
35-
} catch (error) {
36-
console.warn("OAuth metadata discovery failed:", error);
10+
class InspectorOAuthClientProvider implements OAuthClientProvider {
11+
get redirectUrl() {
12+
return window.location.origin + "/oauth/callback";
3713
}
3814

39-
// Fall back to default endpoints
40-
const baseUrl = new URL(serverUrl);
41-
const defaultMetadata = {
42-
authorization_endpoint: new URL("/authorize", baseUrl).toString(),
43-
token_endpoint: new URL("/token", baseUrl).toString(),
44-
};
45-
return OAuthMetadataSchema.parse(defaultMetadata);
46-
}
47-
48-
export async function startOAuthFlow(serverUrl: string): Promise<string> {
49-
// Generate PKCE challenge
50-
const challenge = await pkceChallenge();
51-
const codeVerifier = challenge.code_verifier;
52-
const codeChallenge = challenge.code_challenge;
53-
54-
// Store code verifier for later use
55-
sessionStorage.setItem(SESSION_KEYS.CODE_VERIFIER, codeVerifier);
56-
57-
// Discover OAuth endpoints
58-
const metadata = await discoverOAuthMetadata(serverUrl);
15+
get clientMetadata() {
16+
return {
17+
redirect_uris: [this.redirectUrl],
18+
token_endpoint_auth_method: "none",
19+
grant_types: ["authorization_code", "refresh_token"],
20+
response_types: ["code"],
21+
client_name: "MCP Inspector",
22+
client_uri: "https://github.com/modelcontextprotocol/inspector",
23+
};
24+
}
5925

60-
// Build authorization URL
61-
const authUrl = new URL(metadata.authorization_endpoint);
62-
authUrl.searchParams.set("response_type", "code");
63-
authUrl.searchParams.set("code_challenge", codeChallenge);
64-
authUrl.searchParams.set("code_challenge_method", "S256");
65-
authUrl.searchParams.set(
66-
"redirect_uri",
67-
window.location.origin + "/oauth/callback",
68-
);
26+
async clientInformation() {
27+
const value = sessionStorage.getItem(SESSION_KEYS.CLIENT_INFORMATION);
28+
if (!value) {
29+
return undefined;
30+
}
6931

70-
return authUrl.toString();
71-
}
32+
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
33+
}
7234

73-
export async function handleOAuthCallback(
74-
serverUrl: string,
75-
code: string,
76-
): Promise<OAuthTokens> {
77-
// Get stored code verifier
78-
const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
79-
if (!codeVerifier) {
80-
throw new Error("No code verifier found");
35+
saveClientInformation(clientInformation: OAuthClientInformation) {
36+
sessionStorage.setItem(
37+
SESSION_KEYS.CLIENT_INFORMATION,
38+
JSON.stringify(clientInformation),
39+
);
8140
}
8241

83-
// Discover OAuth endpoints
84-
const metadata = await discoverOAuthMetadata(serverUrl);
85-
// Exchange code for tokens
86-
const response = await fetch(metadata.token_endpoint, {
87-
method: "POST",
88-
headers: {
89-
"Content-Type": "application/x-www-form-urlencoded",
90-
},
91-
body: new URLSearchParams({
92-
grant_type: "authorization_code",
93-
code,
94-
code_verifier: codeVerifier,
95-
redirect_uri: window.location.origin + "/oauth/callback",
96-
}),
97-
});
42+
async tokens() {
43+
const tokens = sessionStorage.getItem(SESSION_KEYS.TOKENS);
44+
if (!tokens) {
45+
return undefined;
46+
}
9847

99-
if (!response.ok) {
100-
throw new Error("Token exchange failed");
48+
return await OAuthTokensSchema.parseAsync(JSON.parse(tokens));
10149
}
10250

103-
const tokens = await response.json();
104-
return OAuthTokensSchema.parse(tokens);
105-
}
51+
saveTokens(tokens: OAuthTokens) {
52+
sessionStorage.setItem(SESSION_KEYS.TOKENS, JSON.stringify(tokens));
53+
}
10654

107-
export async function refreshAccessToken(
108-
serverUrl: string,
109-
): Promise<OAuthTokens> {
110-
const refreshToken = sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN);
111-
if (!refreshToken) {
112-
throw new Error("No refresh token available");
55+
redirectToAuthorization(authorizationUrl: URL) {
56+
window.location.href = authorizationUrl.href;
11357
}
11458

115-
const metadata = await discoverOAuthMetadata(serverUrl);
59+
saveCodeVerifier(codeVerifier: string) {
60+
sessionStorage.setItem(SESSION_KEYS.CODE_VERIFIER, codeVerifier);
61+
}
11662

117-
const response = await fetch(metadata.token_endpoint, {
118-
method: "POST",
119-
headers: {
120-
"Content-Type": "application/x-www-form-urlencoded",
121-
},
122-
body: new URLSearchParams({
123-
grant_type: "refresh_token",
124-
refresh_token: refreshToken,
125-
}),
126-
});
63+
codeVerifier() {
64+
const verifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
65+
if (!verifier) {
66+
throw new Error("No code verifier saved for session");
67+
}
12768

128-
if (!response.ok) {
129-
throw new Error("Token refresh failed");
69+
return verifier;
13070
}
131-
132-
const tokens = await response.json();
133-
return OAuthTokensSchema.parse(tokens);
13471
}
72+
73+
export const authProvider = new InspectorOAuthClientProvider();

client/src/lib/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
export const SESSION_KEYS = {
33
CODE_VERIFIER: "mcp_code_verifier",
44
SERVER_URL: "mcp_server_url",
5-
ACCESS_TOKEN: "mcp_access_token",
6-
REFRESH_TOKEN: "mcp_refresh_token",
5+
TOKENS: "mcp_tokens",
6+
CLIENT_INFORMATION: "mcp_client_information",
77
} as const;

client/src/lib/hooks/useConnection.ts

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import {
2121
import { useState } from "react";
2222
import { toast } from "react-toastify";
2323
import { z } from "zod";
24-
import { startOAuthFlow, refreshAccessToken } from "../auth";
2524
import { SESSION_KEYS } from "../constants";
2625
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
26+
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
27+
import { authProvider } from "../auth";
2728

2829
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
2930

@@ -183,45 +184,14 @@ export function useConnection({
183184
}
184185
};
185186

186-
const initiateOAuthFlow = async () => {
187-
sessionStorage.removeItem(SESSION_KEYS.ACCESS_TOKEN);
188-
sessionStorage.removeItem(SESSION_KEYS.REFRESH_TOKEN);
189-
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
190-
const redirectUrl = await startOAuthFlow(sseUrl);
191-
window.location.href = redirectUrl;
192-
};
193-
194-
const handleTokenRefresh = async () => {
195-
try {
196-
const tokens = await refreshAccessToken(sseUrl);
197-
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
198-
if (tokens.refresh_token) {
199-
sessionStorage.setItem(
200-
SESSION_KEYS.REFRESH_TOKEN,
201-
tokens.refresh_token,
202-
);
203-
}
204-
return tokens.access_token;
205-
} catch (error) {
206-
console.error("Token refresh failed:", error);
207-
await initiateOAuthFlow();
208-
throw error;
209-
}
210-
};
211-
212187
const handleAuthError = async (error: unknown) => {
213188
if (error instanceof SseError && error.code === 401) {
214-
if (sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
215-
try {
216-
await handleTokenRefresh();
217-
return true;
218-
} catch (error) {
219-
console.error("Token refresh failed:", error);
220-
}
221-
} else {
222-
await initiateOAuthFlow();
223-
}
189+
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
190+
191+
const result = await auth(authProvider, { serverUrl: sseUrl });
192+
return result === "AUTHORIZED";
224193
}
194+
225195
return false;
226196
};
227197

@@ -253,10 +223,12 @@ export function useConnection({
253223
backendUrl.searchParams.append("url", sseUrl);
254224
}
255225

226+
// Inject auth manually instead of using SSEClientTransport, because we're
227+
// proxying through the inspector server first.
256228
const headers: HeadersInit = {};
257-
const accessToken = sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN);
258-
if (accessToken) {
259-
headers["Authorization"] = `Bearer ${accessToken}`;
229+
const tokens = await authProvider.tokens();
230+
if (tokens) {
231+
headers["Authorization"] = `Bearer ${tokens.access_token}`;
260232
}
261233

262234
const clientTransport = new SSEClientTransport(backendUrl, {

0 commit comments

Comments
 (0)