mcp-connector
Version:
MCP Remote Proxy Server for Streamable HTTP with OAuth Support.
203 lines (200 loc) • 9.69 kB
JavaScript
import open from "open";
import { exec } from "child_process";
import notifier from "node-notifier";
import { createLogger, logger as DefaultLogger, } from "../utils/file-logger.js";
import { TokenManager } from "../auth/token-manager.js";
import { GLOBAL_SERVER_CONFIGS } from "../cli.js";
export class OAuthDialogManager {
logger = DefaultLogger;
constructor(url) {
let serverName = null;
const hash = TokenManager.hashUrl(url);
if (GLOBAL_SERVER_CONFIGS && GLOBAL_SERVER_CONFIGS.has(hash)) {
serverName = GLOBAL_SERVER_CONFIGS.get(hash).name || null;
}
this.logger = serverName ? createLogger(serverName, hash) : DefaultLogger;
}
async showAuthDialog(url) {
await this.showOSPrompt(url);
}
async showOSPrompt(url) {
this.logger.info(`OAuth authentication URL: ${url}`);
const title = "MCP Connector - OAuth Authentication";
// Truncate URL for display if it's too long
const displayUrl = url.length > 80 ? url.substring(0, 80) + "..." : url;
const message = `OAuth 2.1 Authentication Required\n\nURL: ${displayUrl}\n\nA browser window will open to complete the OAuth flow.\nYou will be redirected to the authorization server to sign in.\n\nChoose an option:`;
if (process.platform.includes("darwin")) {
const script = `tell application "System Events" to display dialog "OAuth 2.1 Authentication Required
URL: ${displayUrl}
A browser window will open to complete the OAuth flow. You will be redirected to the authorization server to sign in.
Choose an option:" with title "${title}" buttons {"Cancel", "Copy URL", "Open Browser"} default button "Open Browser" with icon note`;
const appleScriptRes = await this.executeAppleScript("osascript", ["-e", script], url);
if (appleScriptRes === "cancelled")
process.exit(0);
if (appleScriptRes === "success") {
return;
}
}
try {
notifier.notify({
title: title,
message: message,
wait: true,
actions: ["Close", "Copy URL", "Open Browser"],
closeLabel: "Close",
timeout: 30,
// icon: "random.png"
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(err, response, metadata) => {
console.log(err, response, metadata);
if (err) {
console.error("Error in dialog box");
this.logger.oauth("Error in dialog box");
this.logger.debug("Error in dialog box :: ", err);
}
if (response === "timeout") {
console.log("Dialog timeout exceeded. Opening the browser");
this.logger.oauth("Dialog timeout exceeded. Opening the browser");
this.performBrowserOpen(url);
this.logger.oauth("Spawning browser.");
return;
}
if (response === "close" ||
(metadata && metadata.activationValue === "Close")) {
console.log("OAuth Rejected. Terminating the server");
this.logger.oauth("OAuth Rejected. Terminating the server");
process.exit(0);
}
if (response === "copy url") {
this.copyUrlToClipboard(url);
console.log("OAuth URL copied to clipboard.");
this.logger.oauth("OAuth URL copied to clipboard.");
return;
}
this.performBrowserOpen(url);
this.logger.oauth("Spawning browser.");
});
}
catch (error) {
this.logger.info("Error in notifier. Will be opening browser direclty.");
this.logger.debug("Error in notifier. Will be opening browser direclty. Error :: ", error);
this.performBrowserOpen(url);
}
}
async executeAppleScript(command, args, url) {
return new Promise((resolve, reject) => {
exec(`${command} ${args
.map((arg) => `'${arg.replace(/'/g, "'\"'\"'")}'`)
.join(" ")}`,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error, stdout) => {
console.log(error, stdout);
if (error) {
if (error.message && error.message.includes("User cancelled")) {
this.logger.oauth("Authentication cancelled by user (macOS dialog)");
resolve("cancelled");
return;
}
this.logger.warn("Unable to show OS dialog, falling back to console prompt");
this.logger.debug("Dialog error:", error.message);
reject("failure");
return;
}
const choice = stdout.trim();
console.log(choice);
this.logger.debug(`Dialog choice: "${choice}"`);
if (choice.toLowerCase().includes("open browser")) {
this.performBrowserOpen(url);
}
else if (choice.toLowerCase().includes("copy url")) {
this.copyUrlToClipboard(url);
}
else {
this.logger.oauth("Authentication cancelled by user");
}
resolve("success");
});
});
}
copyUrlToClipboard(url) {
this.logger.progress("Copying URL to clipboard...");
let copyCommand;
switch (process.platform) {
case "darwin": // macOS
copyCommand = `echo "${url}" | pbcopy`;
break;
case "win32": // Windows
copyCommand = `echo "${url}" | clip`;
break;
default: // Linux and others
// Try xclip first, fallback to xsel
copyCommand = `echo "${url}" | xclip -selection clipboard 2>/dev/null || echo "${url}" | xsel --clipboard --input 2>/dev/null`;
break;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
exec(copyCommand, (error) => {
if (error) {
this.logger.error("Could not copy to clipboard automatically");
this.showManualInstructions(url);
this.performBrowserOpen(url);
}
else {
this.logger.success("URL copied to clipboard successfully!");
this.logger.info("");
this.logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
this.logger.info("📋 URL Copied to Clipboard");
this.logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
this.logger.info("");
this.logger.info("✅ The OAuth URL has been copied to your clipboard.");
this.logger.info("📝 Paste it into your browser to complete authentication.");
this.logger.info("");
this.logger.info("⏳ Waiting for authentication to complete...");
this.logger.info(" (This will timeout in 5 minutes)");
this.logger.info("");
}
});
}
showManualInstructions(url) {
this.logger.info("");
this.logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
this.logger.info("📋 Manual Authentication");
this.logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
this.logger.info("");
this.logger.info("Please manually copy and paste this URL into your browser:");
this.logger.info("");
this.logger.info(`🔗 ${url}`);
this.logger.info("");
this.logger.info("⏳ Waiting for authentication to complete...");
this.logger.info(" (This will timeout in 5 minutes)");
this.logger.info("");
}
async performBrowserOpen(url) {
this.logger.progress("Opening browser...");
try {
await open(this.sanitizeUrl(url.toString()));
this.logger.info("Browser opened automatically.");
}
catch (error) {
this.logger.error("Could not open browser automatically. Please copy and paste the URL above into your browser.");
this.logger.debug("Failed to open browser", error);
}
}
// Only localhost is allowed as callback
sanitizeUrl(raw) {
try {
const url = new URL(raw, "http://localhost");
if (url.protocol === "http:" ||
url.protocol === "https:" ||
url.hostname === "localhost" ||
url.hostname === "127.0.0.1") {
return url.toString();
}
return "about:blank";
}
catch {
return "about:blank";
}
}
}
//# sourceMappingURL=dialog-manager.js.map