shreyask's picture
add mcp support
d63a244 verified
raw
history blame
8.58 kB
import {
discoverOAuthProtectedResourceMetadata,
discoverAuthorizationServerMetadata,
startAuthorization,
exchangeAuthorization,
registerClient,
} from "@modelcontextprotocol/sdk/client/auth.js";
import { secureStorage } from "../utils/storage";
import { MCP_CLIENT_CONFIG, STORAGE_KEYS, DEFAULTS } from "../config/constants";
// Utility to fetch .well-known/modelcontextprotocol for OAuth endpoints
export async function discoverOAuthEndpoints(serverUrl: string) {
// ...existing code...
let resourceMetadata, authMetadata, authorizationServerUrl;
try {
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl);
if (resourceMetadata?.authorization_servers?.length) {
authorizationServerUrl = resourceMetadata.authorization_servers[0];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// Fallback to direct metadata discovery if protected resource fails
authMetadata = await discoverAuthorizationServerMetadata(serverUrl);
authorizationServerUrl = authMetadata?.issuer || serverUrl;
}
if (!authorizationServerUrl) {
throw new Error("No authorization server found for this MCP server");
}
// Discover authorization server metadata if not already done
if (!authMetadata) {
authMetadata = await discoverAuthorizationServerMetadata(
authorizationServerUrl
);
}
if (
!authMetadata ||
!authMetadata.authorization_endpoint ||
!authMetadata.token_endpoint
) {
throw new Error("Missing OAuth endpoints in authorization server metadata");
}
// If client_id is missing, register client dynamically
if (!authMetadata.client_id && authMetadata.registration_endpoint) {
// Determine token endpoint auth method
let tokenEndpointAuthMethod = "none";
if (
authMetadata.token_endpoint_auth_methods_supported?.includes(
"client_secret_post"
)
) {
tokenEndpointAuthMethod = "client_secret_post";
} else if (
authMetadata.token_endpoint_auth_methods_supported?.includes(
"client_secret_basic"
)
) {
tokenEndpointAuthMethod = "client_secret_basic";
}
const clientMetadata = {
redirect_uris: [
String(
authMetadata.redirect_uri ||
window.location.origin + "/oauth/callback"
),
],
client_name: MCP_CLIENT_CONFIG.NAME,
grant_types: ["authorization_code"],
response_types: ["code"],
token_endpoint_auth_method: tokenEndpointAuthMethod,
};
const clientInfo = await registerClient(authorizationServerUrl, {
metadata: authMetadata,
clientMetadata,
});
authMetadata.client_id = clientInfo.client_id;
if (clientInfo.client_secret) {
authMetadata.client_secret = clientInfo.client_secret;
}
// Persist client credentials for later use
localStorage.setItem(STORAGE_KEYS.OAUTH_CLIENT_ID, clientInfo.client_id);
if (clientInfo.client_secret) {
await secureStorage.setItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET, clientInfo.client_secret);
}
}
if (!authMetadata.client_id) {
throw new Error(
"Missing client_id and registration not supported by authorization server"
);
}
// Step 3: Validate resource
const resource = resourceMetadata?.resource
? new URL(resourceMetadata.resource)
: undefined;
// Persist endpoints, metadata, and MCP server URL for callback use
localStorage.setItem(
STORAGE_KEYS.OAUTH_AUTHORIZATION_ENDPOINT,
authMetadata.authorization_endpoint
);
localStorage.setItem(STORAGE_KEYS.OAUTH_TOKEN_ENDPOINT, authMetadata.token_endpoint);
localStorage.setItem(
STORAGE_KEYS.OAUTH_REDIRECT_URI,
(authMetadata.redirect_uri || window.location.origin + DEFAULTS.OAUTH_REDIRECT_PATH).toString()
);
localStorage.setItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL, serverUrl);
localStorage.setItem(
STORAGE_KEYS.OAUTH_AUTHORIZATION_SERVER_METADATA,
JSON.stringify(authMetadata)
);
if (resource) {
localStorage.setItem(STORAGE_KEYS.OAUTH_RESOURCE, resource.toString());
}
return {
authorizationEndpoint: authMetadata.authorization_endpoint,
tokenEndpoint: authMetadata.token_endpoint,
clientId: authMetadata.client_id,
clientSecret: authMetadata.client_secret,
scopes: authMetadata.scopes || [],
redirectUri:
authMetadata.redirect_uri || window.location.origin + "/oauth/callback",
resource,
};
}
// Start OAuth flow: redirect user to authorization endpoint
export async function startOAuthFlow({
authorizationEndpoint,
clientId,
redirectUri,
scopes,
resource,
}: {
authorizationEndpoint: string;
clientId: string;
redirectUri: string;
scopes?: string[];
resource?: URL;
}) {
// Use Proof Key for Code Exchange (PKCE) and SDK to build the authorization URL
// Use persisted client_id if available
const persistedClientId = localStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_ID) || clientId;
const clientInformation = { client_id: persistedClientId };
// Retrieve metadata from localStorage if available
let metadata;
try {
const stored = localStorage.getItem(STORAGE_KEYS.OAUTH_AUTHORIZATION_SERVER_METADATA);
if (stored) metadata = JSON.parse(stored);
} catch {
console.warn("Failed to parse stored OAuth metadata, using defaults");
}
// Always pass resource from localStorage if not provided
let resourceParam = resource;
if (!resourceParam) {
const resourceStr = localStorage.getItem(STORAGE_KEYS.OAUTH_RESOURCE);
if (resourceStr) resourceParam = new URL(resourceStr);
}
const { authorizationUrl, codeVerifier } = await startAuthorization(
authorizationEndpoint,
{
metadata,
clientInformation,
redirectUrl: redirectUri,
scope: scopes?.join(" ") || undefined,
resource: resourceParam,
}
);
// Save codeVerifier in localStorage for later token exchange
localStorage.setItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER, codeVerifier);
window.location.href = authorizationUrl.toString();
}
// Exchange code for token using MCP SDK
export async function exchangeCodeForToken({
code,
redirectUri,
}: {
serverUrl?: string;
code: string;
redirectUri: string;
}) {
// Use only persisted credentials and endpoints for token exchange
const tokenEndpoint = localStorage.getItem(STORAGE_KEYS.OAUTH_TOKEN_ENDPOINT);
const redirectUriPersisted = localStorage.getItem(STORAGE_KEYS.OAUTH_REDIRECT_URI);
const resourceStr = localStorage.getItem(STORAGE_KEYS.OAUTH_RESOURCE);
const persistedClientId = localStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_ID);
const persistedClientSecret = await secureStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET);
const codeVerifier = localStorage.getItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER);
if (!persistedClientId || !tokenEndpoint || !codeVerifier)
throw new Error(
"Missing OAuth client credentials or endpoints for token exchange"
);
const clientInformation: { client_id: string; client_secret?: string } = { client_id: persistedClientId };
if (persistedClientSecret) {
clientInformation.client_secret = persistedClientSecret;
}
// Retrieve metadata from localStorage if available
let metadata;
try {
const stored = localStorage.getItem(STORAGE_KEYS.OAUTH_AUTHORIZATION_SERVER_METADATA);
if (stored) metadata = JSON.parse(stored);
} catch {
console.warn("Failed to parse stored OAuth metadata, using defaults");
}
// Use SDK to exchange code for tokens
const tokens = await exchangeAuthorization(tokenEndpoint, {
metadata,
clientInformation,
authorizationCode: code,
codeVerifier,
redirectUri: redirectUriPersisted || redirectUri,
resource: resourceStr ? new URL(resourceStr) : undefined,
});
// Persist access token in localStorage and sync to mcp-servers
if (tokens && tokens.access_token) {
await secureStorage.setItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN, tokens.access_token);
try {
const serversStr = localStorage.getItem(STORAGE_KEYS.MCP_SERVERS);
if (serversStr) {
const servers = JSON.parse(serversStr);
for (const server of servers) {
if (
server.auth &&
(server.auth.type === "bearer" || server.auth.type === "oauth")
) {
server.auth.token = tokens.access_token;
}
}
localStorage.setItem(STORAGE_KEYS.MCP_SERVERS, JSON.stringify(servers));
}
} catch (err) {
console.warn("Failed to sync token to mcp-servers:", err);
}
}
return tokens;
}