mcp-use
Version:
Opinionated MCP Framework for TypeScript (@modelcontextprotocol/sdk compatible) - Build MCP Agents, Clients and Servers with support for ChatGPT Apps, Code Mode, OAuth, Notifications, Sampling, Observability and more.
450 lines (445 loc) • 16.6 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/auth/index.ts
var auth_exports = {};
__export(auth_exports, {
BrowserOAuthClientProvider: () => BrowserOAuthClientProvider,
onMcpAuthorization: () => onMcpAuthorization
});
module.exports = __toCommonJS(auth_exports);
// src/utils/url-sanitize.ts
function sanitizeUrl(raw) {
const abort = /* @__PURE__ */ __name(() => {
throw new Error(`Invalid url to pass to open(): ${raw}`);
}, "abort");
let url;
try {
url = new URL(raw);
} catch (_) {
abort();
}
if (url.protocol !== "https:" && url.protocol !== "http:") abort();
if (url.hostname !== encodeURIComponent(url.hostname)) abort();
if (url.username) url.username = encodeURIComponent(url.username);
if (url.password) url.password = encodeURIComponent(url.password);
url.pathname = url.pathname.slice(0, 1) + encodeURIComponent(url.pathname.slice(1)).replace(/%2f/gi, "/");
url.search = url.search.slice(0, 1) + Array.from(url.searchParams.entries()).map(sanitizeParam).join("&");
url.hash = url.hash.slice(0, 1) + encodeURIComponent(url.hash.slice(1));
return url.href;
}
__name(sanitizeUrl, "sanitizeUrl");
function sanitizeParam([k, v]) {
return `${encodeURIComponent(k)}${v.length > 0 ? `=${encodeURIComponent(v)}` : ""}`;
}
__name(sanitizeParam, "sanitizeParam");
// src/auth/browser-provider.ts
var BrowserOAuthClientProvider = class {
static {
__name(this, "BrowserOAuthClientProvider");
}
serverUrl;
storageKeyPrefix;
serverUrlHash;
clientName;
clientUri;
callbackUrl;
preventAutoAuth;
useRedirectFlow;
onPopupWindow;
constructor(serverUrl, options = {}) {
this.serverUrl = serverUrl;
this.storageKeyPrefix = options.storageKeyPrefix || "mcp:auth";
this.serverUrlHash = this.hashString(serverUrl);
this.clientName = options.clientName || "mcp-use";
this.clientUri = options.clientUri || (typeof window !== "undefined" ? window.location.origin : "");
this.callbackUrl = sanitizeUrl(
options.callbackUrl || (typeof window !== "undefined" ? new URL("/oauth/callback", window.location.origin).toString() : "/oauth/callback")
);
this.preventAutoAuth = options.preventAutoAuth;
this.useRedirectFlow = options.useRedirectFlow;
this.onPopupWindow = options.onPopupWindow;
}
// --- SDK Interface Methods ---
get redirectUrl() {
return sanitizeUrl(this.callbackUrl);
}
get clientMetadata() {
return {
redirect_uris: [this.redirectUrl],
token_endpoint_auth_method: "none",
// Public client
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
client_name: this.clientName,
client_uri: this.clientUri
// scope: 'openid profile email mcp', // Example scopes, adjust as needed
};
}
async clientInformation() {
const key = this.getKey("client_info");
const data = localStorage.getItem(key);
if (!data) return void 0;
try {
return JSON.parse(data);
} catch (e) {
console.warn(
`[${this.storageKeyPrefix}] Failed to parse client information:`,
e
);
localStorage.removeItem(key);
return void 0;
}
}
// NOTE: The SDK's auth() function uses this if dynamic registration is needed.
// Ensure your OAuthClientInformationFull matches the expected structure if DCR is used.
async saveClientInformation(clientInformation) {
const key = this.getKey("client_info");
localStorage.setItem(key, JSON.stringify(clientInformation));
}
async tokens() {
const key = this.getKey("tokens");
const data = localStorage.getItem(key);
if (!data) return void 0;
try {
return JSON.parse(data);
} catch (e) {
console.warn(`[${this.storageKeyPrefix}] Failed to parse tokens:`, e);
localStorage.removeItem(key);
return void 0;
}
}
async saveTokens(tokens) {
const key = this.getKey("tokens");
localStorage.setItem(key, JSON.stringify(tokens));
localStorage.removeItem(this.getKey("code_verifier"));
localStorage.removeItem(this.getKey("last_auth_url"));
}
async saveCodeVerifier(codeVerifier) {
const key = this.getKey("code_verifier");
localStorage.setItem(key, codeVerifier);
}
async codeVerifier() {
const key = this.getKey("code_verifier");
const verifier = localStorage.getItem(key);
if (!verifier) {
throw new Error(
`[${this.storageKeyPrefix}] Code verifier not found in storage for key ${key}. Auth flow likely corrupted or timed out.`
);
}
return verifier;
}
/**
* Generates and stores the authorization URL with state, without opening a popup.
* Used when preventAutoAuth is enabled to provide the URL for manual navigation.
* @param authorizationUrl The fully constructed authorization URL from the SDK.
* @returns The full authorization URL with state parameter.
*/
async prepareAuthorizationUrl(authorizationUrl) {
const state = globalThis.crypto.randomUUID();
const stateKey = `${this.storageKeyPrefix}:state_${state}`;
const stateData = {
serverUrlHash: this.serverUrlHash,
expiry: Date.now() + 1e3 * 60 * 10,
// State expires in 10 minutes
// Store provider options needed to reconstruct on callback
providerOptions: {
serverUrl: this.serverUrl,
storageKeyPrefix: this.storageKeyPrefix,
clientName: this.clientName,
clientUri: this.clientUri,
callbackUrl: this.callbackUrl
},
// Store flow type so callback knows how to handle the response
flowType: this.useRedirectFlow ? "redirect" : "popup",
// Store current URL for redirect flow so we can return to it
returnUrl: this.useRedirectFlow && typeof window !== "undefined" ? window.location.href : void 0
};
localStorage.setItem(stateKey, JSON.stringify(stateData));
authorizationUrl.searchParams.set("state", state);
const authUrlString = authorizationUrl.toString();
const sanitizedAuthUrl = sanitizeUrl(authUrlString);
localStorage.setItem(this.getKey("last_auth_url"), sanitizedAuthUrl);
return sanitizedAuthUrl;
}
/**
* Redirects the user agent to the authorization URL, storing necessary state.
* This now adheres to the SDK's void return type expectation for the interface.
* @param authorizationUrl The fully constructed authorization URL from the SDK.
*/
async redirectToAuthorization(authorizationUrl) {
const sanitizedAuthUrl = await this.prepareAuthorizationUrl(authorizationUrl);
if (this.preventAutoAuth) {
console.info(
`[${this.storageKeyPrefix}] Auto-auth prevented. Authorization URL stored for manual trigger.`
);
return;
}
if (this.useRedirectFlow) {
console.info(
`[${this.storageKeyPrefix}] Redirecting to authorization URL (full-page redirect).`
);
window.location.href = sanitizedAuthUrl;
return;
}
const popupFeatures = "width=600,height=700,resizable=yes,scrollbars=yes,status=yes";
try {
const popup = window.open(
sanitizedAuthUrl,
`mcp_auth_${this.serverUrlHash}`,
popupFeatures
);
if (this.onPopupWindow) {
this.onPopupWindow(sanitizedAuthUrl, popupFeatures, popup);
}
if (!popup || popup.closed || typeof popup.closed === "undefined") {
console.warn(
`[${this.storageKeyPrefix}] Popup likely blocked by browser. Manual navigation might be required using the stored URL.`
);
} else {
popup.focus();
console.info(
`[${this.storageKeyPrefix}] Redirecting to authorization URL in popup.`
);
}
} catch (e) {
console.error(
`[${this.storageKeyPrefix}] Error opening popup window:`,
e
);
}
}
// --- Helper Methods ---
/**
* Retrieves the last URL passed to `redirectToAuthorization`. Useful for manual fallback.
*/
getLastAttemptedAuthUrl() {
const storedUrl = localStorage.getItem(this.getKey("last_auth_url"));
return storedUrl ? sanitizeUrl(storedUrl) : null;
}
clearStorage() {
const prefixPattern = `${this.storageKeyPrefix}_${this.serverUrlHash}_`;
const statePattern = `${this.storageKeyPrefix}:state_`;
const keysToRemove = [];
let count = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key) continue;
if (key.startsWith(prefixPattern)) {
keysToRemove.push(key);
} else if (key.startsWith(statePattern)) {
try {
const item = localStorage.getItem(key);
if (item) {
const state = JSON.parse(item);
if (state.serverUrlHash === this.serverUrlHash) {
keysToRemove.push(key);
}
}
} catch (e) {
console.warn(
`[${this.storageKeyPrefix}] Error parsing state key ${key} during clearStorage:`,
e
);
}
}
}
const uniqueKeysToRemove = [...new Set(keysToRemove)];
uniqueKeysToRemove.forEach((key) => {
localStorage.removeItem(key);
count++;
});
return count;
}
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
getKey(keySuffix) {
return `${this.storageKeyPrefix}_${this.serverUrlHash}_${keySuffix}`;
}
};
// src/auth/callback.ts
var import_auth = require("@mcp-use/modelcontextprotocol-sdk/client/auth.js");
async function onMcpAuthorization() {
const queryParams = new URLSearchParams(window.location.search);
const code = queryParams.get("code");
const state = queryParams.get("state");
const error = queryParams.get("error");
const errorDescription = queryParams.get("error_description");
const logPrefix = "[mcp-callback]";
console.log(`${logPrefix} Handling callback...`, {
code,
state,
error,
errorDescription
});
let provider = null;
let storedStateData = null;
const stateKey = state ? `mcp:auth:state_${state}` : null;
try {
if (error) {
throw new Error(
`OAuth error: ${error} - ${errorDescription || "No description provided."}`
);
}
if (!code) {
throw new Error(
"Authorization code not found in callback query parameters."
);
}
if (!state || !stateKey) {
throw new Error(
"State parameter not found or invalid in callback query parameters."
);
}
const storedStateJSON = localStorage.getItem(stateKey);
if (!storedStateJSON) {
throw new Error(
`Invalid or expired state parameter "${state}". No matching state found in storage.`
);
}
try {
storedStateData = JSON.parse(storedStateJSON);
} catch (e) {
throw new Error("Failed to parse stored OAuth state.");
}
if (!storedStateData.expiry || storedStateData.expiry < Date.now()) {
localStorage.removeItem(stateKey);
throw new Error(
"OAuth state has expired. Please try initiating authentication again."
);
}
if (!storedStateData.providerOptions) {
throw new Error("Stored state is missing required provider options.");
}
const { serverUrl, ...providerOptions } = storedStateData.providerOptions;
console.log(
`${logPrefix} Re-instantiating provider for server: ${serverUrl}`
);
provider = new BrowserOAuthClientProvider(serverUrl, providerOptions);
console.log(`${logPrefix} Calling SDK auth() to exchange code...`);
const baseUrl = new URL(serverUrl).origin;
const authResult = await (0, import_auth.auth)(provider, {
serverUrl: baseUrl,
authorizationCode: code
});
if (authResult === "AUTHORIZED") {
console.log(`${logPrefix} Authorization successful via SDK auth().`);
const isRedirectFlow = storedStateData.flowType === "redirect";
if (isRedirectFlow && storedStateData.returnUrl) {
console.log(
`${logPrefix} Redirect flow complete. Returning to: ${storedStateData.returnUrl}`
);
localStorage.removeItem(stateKey);
window.location.href = storedStateData.returnUrl;
} else if (window.opener && !window.opener.closed) {
console.log(`${logPrefix} Popup flow complete. Notifying opener...`);
window.opener.postMessage(
{ type: "mcp_auth_callback", success: true },
window.location.origin
);
localStorage.removeItem(stateKey);
window.close();
} else {
console.warn(
`${logPrefix} No opener window or return URL detected. Redirecting to root.`
);
localStorage.removeItem(stateKey);
const pathParts = window.location.pathname.split("/").filter(Boolean);
const basePath = pathParts.length > 0 && pathParts[pathParts.length - 1] === "callback" ? "/" + pathParts.slice(0, -2).join("/") : "/";
window.location.href = basePath || "/";
}
} else {
console.warn(
`${logPrefix} SDK auth() returned unexpected status: ${authResult}`
);
throw new Error(
`Unexpected result from authentication library: ${authResult}`
);
}
} catch (err) {
console.error(`${logPrefix} Error during OAuth callback handling:`, err);
const errorMessage = err instanceof Error ? err.message : String(err);
if (window.opener && !window.opener.closed) {
window.opener.postMessage(
{ type: "mcp_auth_callback", success: false, error: errorMessage },
window.location.origin
);
}
try {
document.body.innerHTML = "";
const container = document.createElement("div");
container.style.fontFamily = "sans-serif";
container.style.padding = "20px";
const heading = document.createElement("h1");
heading.textContent = "Authentication Error";
container.appendChild(heading);
const errorPara = document.createElement("p");
errorPara.style.color = "red";
errorPara.style.backgroundColor = "#ffebeb";
errorPara.style.border = "1px solid red";
errorPara.style.padding = "10px";
errorPara.style.borderRadius = "4px";
errorPara.textContent = errorMessage;
container.appendChild(errorPara);
const closePara = document.createElement("p");
closePara.textContent = "You can close this window or ";
const closeLink = document.createElement("a");
closeLink.href = "#";
closeLink.textContent = "click here to close";
closeLink.onclick = (e) => {
e.preventDefault();
window.close();
return false;
};
closePara.appendChild(closeLink);
closePara.appendChild(document.createTextNode("."));
container.appendChild(closePara);
if (err instanceof Error && err.stack) {
const stackPre = document.createElement("pre");
stackPre.style.fontSize = "0.8em";
stackPre.style.color = "#555";
stackPre.style.marginTop = "20px";
stackPre.style.whiteSpace = "pre-wrap";
stackPre.textContent = err.stack;
container.appendChild(stackPre);
}
document.body.appendChild(container);
} catch (displayError) {
console.error(
`${logPrefix} Could not display error in callback window:`,
displayError
);
}
if (stateKey) {
localStorage.removeItem(stateKey);
}
if (provider) {
localStorage.removeItem(provider.getKey("code_verifier"));
localStorage.removeItem(provider.getKey("last_auth_url"));
}
}
}
__name(onMcpAuthorization, "onMcpAuthorization");