mcp-use
Version:
Opinionated MCP Framework for TypeScript (@modelcontextprotocol/sdk compatible) - Build MCP Agents and Clients + MCP Servers with support for MCP-UI.
1,412 lines (1,395 loc) • 44.4 kB
JavaScript
import {
logger
} from "./chunk-34R6SIER.js";
import {
__name
} from "./chunk-3GQAWCBQ.js";
// src/session.ts
var MCPSession = class {
static {
__name(this, "MCPSession");
}
connector;
autoConnect;
constructor(connector, autoConnect = true) {
this.connector = connector;
this.autoConnect = autoConnect;
}
async connect() {
await this.connector.connect();
}
async disconnect() {
await this.connector.disconnect();
}
async initialize() {
if (!this.isConnected && this.autoConnect) {
await this.connect();
}
await this.connector.initialize();
}
get isConnected() {
return this.connector && this.connector.isClientConnected;
}
};
// src/connectors/base.ts
var BaseConnector = class {
static {
__name(this, "BaseConnector");
}
client = null;
connectionManager = null;
toolsCache = null;
capabilitiesCache = null;
connected = false;
opts;
constructor(opts = {}) {
this.opts = opts;
}
/** Disconnect and release resources. */
async disconnect() {
if (!this.connected) {
logger.debug("Not connected to MCP implementation");
return;
}
logger.debug("Disconnecting from MCP implementation");
await this.cleanupResources();
this.connected = false;
logger.debug("Disconnected from MCP implementation");
}
/** Check if the client is connected */
get isClientConnected() {
return this.client != null;
}
/**
* Initialise the MCP session **after** `connect()` has succeeded.
*
* In the SDK, `Client.connect(transport)` automatically performs the
* protocol‑level `initialize` handshake, so we only need to cache the list of
* tools and expose some server info.
*/
async initialize(defaultRequestOptions = this.opts.defaultRequestOptions ?? {}) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug("Caching server capabilities & tools");
const capabilities = this.client.getServerCapabilities();
this.capabilitiesCache = capabilities;
const listToolsRes = await this.client.listTools(
void 0,
defaultRequestOptions
);
this.toolsCache = listToolsRes.tools ?? [];
logger.debug(`Fetched ${this.toolsCache.length} tools from server`);
logger.debug("Server capabilities:", capabilities);
return capabilities;
}
/** Lazily expose the cached tools list. */
get tools() {
if (!this.toolsCache) {
throw new Error("MCP client is not initialized; call initialize() first");
}
return this.toolsCache;
}
/** Call a tool on the server. */
async callTool(name, args, options) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug(`Calling tool '${name}' with args`, args);
const res = await this.client.callTool(
{ name, arguments: args },
void 0,
options
);
logger.debug(`Tool '${name}' returned`, res);
return res;
}
/**
* List resources from the server with optional pagination
*
* @param cursor - Optional cursor for pagination
* @param options - Request options
* @returns Resource list with optional nextCursor for pagination
*/
async listResources(cursor, options) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug("Listing resources", cursor ? `with cursor: ${cursor}` : "");
return await this.client.listResources({ cursor }, options);
}
/**
* List all resources from the server, automatically handling pagination
*
* @param options - Request options
* @returns Complete list of all resources
*/
async listAllResources(options) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
if (!this.capabilitiesCache?.resources) {
logger.debug("Server does not advertise resources capability, skipping");
return { resources: [] };
}
try {
logger.debug("Listing all resources (with auto-pagination)");
const allResources = [];
let cursor = void 0;
do {
const result = await this.client.listResources({ cursor }, options);
allResources.push(...result.resources || []);
cursor = result.nextCursor;
} while (cursor);
return { resources: allResources };
} catch (err) {
if (err.code === -32601) {
logger.debug("Server advertised resources but method not found");
return { resources: [] };
}
throw err;
}
}
/**
* List resource templates from the server
*
* @param options - Request options
* @returns List of available resource templates
*/
async listResourceTemplates(options) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug("Listing resource templates");
return await this.client.listResourceTemplates(void 0, options);
}
/** Read a resource by URI. */
async readResource(uri, options) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug(`Reading resource ${uri}`);
const res = await this.client.readResource({ uri }, options);
return res;
}
/**
* Subscribe to resource updates
*
* @param uri - URI of the resource to subscribe to
* @param options - Request options
*/
async subscribeToResource(uri, options) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug(`Subscribing to resource: ${uri}`);
return await this.client.subscribeResource({ uri }, options);
}
/**
* Unsubscribe from resource updates
*
* @param uri - URI of the resource to unsubscribe from
* @param options - Request options
*/
async unsubscribeFromResource(uri, options) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug(`Unsubscribing from resource: ${uri}`);
return await this.client.unsubscribeResource({ uri }, options);
}
async listPrompts() {
if (!this.client) {
throw new Error("MCP client is not connected");
}
if (!this.capabilitiesCache?.prompts) {
logger.debug("Server does not advertise prompts capability, skipping");
return { prompts: [] };
}
try {
logger.debug("Listing prompts");
return await this.client.listPrompts();
} catch (err) {
if (err.code === -32601) {
logger.debug("Server advertised prompts but method not found");
return { prompts: [] };
}
throw err;
}
}
async getPrompt(name, args) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug(`Getting prompt ${name}`);
return await this.client.getPrompt({ name, arguments: args });
}
/** Send a raw request through the client. */
async request(method, params = null, options) {
if (!this.client) {
throw new Error("MCP client is not connected");
}
logger.debug(`Sending raw request '${method}' with params`, params);
return await this.client.request(
{ method, params: params ?? {} },
void 0,
options
);
}
/**
* Helper to tear down the client & connection manager safely.
*/
async cleanupResources() {
const issues = [];
if (this.client) {
try {
if (typeof this.client.close === "function") {
await this.client.close();
}
} catch (e) {
const msg = `Error closing client: ${e}`;
logger.warn(msg);
issues.push(msg);
} finally {
this.client = null;
}
}
if (this.connectionManager) {
try {
await this.connectionManager.stop();
} catch (e) {
const msg = `Error stopping connection manager: ${e}`;
logger.warn(msg);
issues.push(msg);
} finally {
this.connectionManager = null;
}
}
this.toolsCache = null;
if (issues.length) {
logger.warn(`Resource cleanup finished with ${issues.length} issue(s)`);
}
}
};
// src/connectors/http.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
// src/task_managers/sse.ts
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
// src/task_managers/base.ts
var ConnectionManager = class {
static {
__name(this, "ConnectionManager");
}
_readyPromise;
_readyResolver;
_donePromise;
_doneResolver;
_exception = null;
_connection = null;
_task = null;
_abortController = null;
constructor() {
this.reset();
}
/**
* Start the connection manager and establish a connection.
*
* @returns The established connection.
* @throws If the connection cannot be established.
*/
async start() {
this.reset();
logger.debug(`Starting ${this.constructor.name}`);
this._task = this.connectionTask();
await this._readyPromise;
if (this._exception) {
throw this._exception;
}
if (this._connection === null) {
throw new Error("Connection was not established");
}
return this._connection;
}
/**
* Stop the connection manager and close the connection.
*/
async stop() {
if (this._task && this._abortController) {
logger.debug(`Cancelling ${this.constructor.name} task`);
this._abortController.abort();
try {
await this._task;
} catch (e) {
if (e instanceof Error && e.name === "AbortError") {
logger.debug(`${this.constructor.name} task aborted successfully`);
} else {
logger.warn(`Error stopping ${this.constructor.name} task: ${e}`);
}
}
}
await this._donePromise;
logger.debug(`${this.constructor.name} task completed`);
}
/**
* Reset all internal state.
*/
reset() {
this._readyPromise = new Promise((res) => this._readyResolver = res);
this._donePromise = new Promise((res) => this._doneResolver = res);
this._exception = null;
this._connection = null;
this._task = null;
this._abortController = new AbortController();
}
/**
* The background task responsible for establishing and maintaining the
* connection until it is cancelled.
*/
async connectionTask() {
logger.debug(`Running ${this.constructor.name} task`);
try {
this._connection = await this.establishConnection();
logger.debug(`${this.constructor.name} connected successfully`);
this._readyResolver();
await this.waitForAbort();
} catch (err) {
this._exception = err;
logger.error(`Error in ${this.constructor.name} task: ${err}`);
this._readyResolver();
} finally {
if (this._connection !== null) {
try {
await this.closeConnection(this._connection);
} catch (closeErr) {
logger.warn(
`Error closing connection in ${this.constructor.name}: ${closeErr}`
);
}
this._connection = null;
}
this._doneResolver();
}
}
/**
* Helper that returns a promise which resolves when the abort signal fires.
*/
async waitForAbort() {
return new Promise((_resolve, _reject) => {
if (!this._abortController) {
return;
}
const signal = this._abortController.signal;
if (signal.aborted) {
_resolve();
return;
}
const onAbort = /* @__PURE__ */ __name(() => {
signal.removeEventListener("abort", onAbort);
_resolve();
}, "onAbort");
signal.addEventListener("abort", onAbort);
});
}
};
// src/task_managers/sse.ts
var SseConnectionManager = class extends ConnectionManager {
static {
__name(this, "SseConnectionManager");
}
url;
opts;
_transport = null;
/**
* Create an SSE connection manager.
*
* @param url The SSE endpoint URL.
* @param opts Optional transport options (auth, headers, etc.).
*/
constructor(url, opts) {
super();
this.url = typeof url === "string" ? new URL(url) : url;
this.opts = opts;
}
/**
* Spawn a new `SSEClientTransport` and start the connection.
*/
async establishConnection() {
this._transport = new SSEClientTransport(this.url, this.opts);
logger.debug(`${this.constructor.name} connected successfully`);
return this._transport;
}
/**
* Close the underlying transport and clean up resources.
*/
async closeConnection(_connection) {
if (this._transport) {
try {
await this._transport.close();
} catch (e) {
logger.warn(`Error closing SSE transport: ${e}`);
} finally {
this._transport = null;
}
}
}
};
// src/task_managers/streamable_http.ts
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
var StreamableHttpConnectionManager = class extends ConnectionManager {
static {
__name(this, "StreamableHttpConnectionManager");
}
url;
opts;
_transport = null;
/**
* Create a Streamable HTTP connection manager.
*
* @param url The HTTP endpoint URL.
* @param opts Optional transport options (auth, headers, etc.).
*/
constructor(url, opts) {
super();
this.url = typeof url === "string" ? new URL(url) : url;
this.opts = opts;
}
/**
* Spawn a new `StreamableHTTPClientTransport` and return it.
* The Client.connect() method will handle starting the transport.
*/
async establishConnection() {
this._transport = new StreamableHTTPClientTransport(this.url, this.opts);
logger.debug(`${this.constructor.name} created successfully`);
return this._transport;
}
/**
* Close the underlying transport and clean up resources.
*/
async closeConnection(_connection) {
if (this._transport) {
try {
await this._transport.close();
} catch (e) {
logger.warn(`Error closing Streamable HTTP transport: ${e}`);
} finally {
this._transport = null;
}
}
}
/**
* Get the session ID from the transport if available.
*/
get sessionId() {
return this._transport?.sessionId;
}
};
// src/connectors/http.ts
var HttpConnector = class extends BaseConnector {
static {
__name(this, "HttpConnector");
}
baseUrl;
headers;
timeout;
sseReadTimeout;
clientInfo;
preferSse;
transportType = null;
constructor(baseUrl, opts = {}) {
super(opts);
this.baseUrl = baseUrl.replace(/\/$/, "");
this.headers = { ...opts.headers ?? {} };
if (opts.authToken) {
this.headers.Authorization = `Bearer ${opts.authToken}`;
}
this.timeout = opts.timeout ?? 3e4;
this.sseReadTimeout = opts.sseReadTimeout ?? 3e5;
this.clientInfo = opts.clientInfo ?? {
name: "http-connector",
version: "1.0.0"
};
this.preferSse = opts.preferSse ?? false;
}
/** Establish connection to the MCP implementation via HTTP (streamable or SSE). */
async connect() {
if (this.connected) {
logger.debug("Already connected to MCP implementation");
return;
}
const baseUrl = this.baseUrl;
if (this.preferSse) {
logger.debug(`Connecting to MCP implementation via HTTP/SSE: ${baseUrl}`);
await this.connectWithSse(baseUrl);
return;
}
logger.debug(`Connecting to MCP implementation via HTTP: ${baseUrl}`);
try {
logger.info("\u{1F504} Attempting streamable HTTP transport...");
await this.connectWithStreamableHttp(baseUrl);
logger.info("\u2705 Successfully connected via streamable HTTP");
} catch (err) {
let fallbackReason = "Unknown error";
let is401Error = false;
if (err instanceof StreamableHTTPError) {
is401Error = err.code === 401;
if (err.code === 400 && err.message.includes("Missing session ID")) {
fallbackReason = "Server requires session ID (FastMCP compatibility) - using SSE transport";
logger.warn(`\u26A0\uFE0F ${fallbackReason}`);
} else if (err.code === 404 || err.code === 405) {
fallbackReason = `Server returned ${err.code} - server likely doesn't support streamable HTTP`;
logger.debug(fallbackReason);
} else {
fallbackReason = `Server returned ${err.code}: ${err.message}`;
logger.debug(fallbackReason);
}
} else if (err instanceof Error) {
const errorStr = err.toString();
const errorMsg = err.message || "";
is401Error = errorStr.includes("401") || errorMsg.includes("Unauthorized");
if (errorStr.includes("Missing session ID") || errorStr.includes("Bad Request: Missing session ID") || errorMsg.includes("FastMCP session ID error")) {
fallbackReason = "Server requires session ID (FastMCP compatibility) - using SSE transport";
logger.warn(`\u26A0\uFE0F ${fallbackReason}`);
} else if (errorStr.includes("405 Method Not Allowed") || errorStr.includes("404 Not Found")) {
fallbackReason = "Server doesn't support streamable HTTP (405/404)";
logger.debug(fallbackReason);
} else {
fallbackReason = `Streamable HTTP failed: ${err.message}`;
logger.debug(fallbackReason);
}
}
if (is401Error) {
logger.info("Authentication required - skipping SSE fallback");
await this.cleanupResources();
const authError = new Error("Authentication required");
authError.code = 401;
throw authError;
}
logger.info("\u{1F504} Falling back to SSE transport...");
try {
await this.connectWithSse(baseUrl);
} catch (sseErr) {
logger.error(`Failed to connect with both transports:`);
logger.error(` Streamable HTTP: ${fallbackReason}`);
logger.error(` SSE: ${sseErr}`);
await this.cleanupResources();
const sseIs401 = sseErr?.message?.includes("401") || sseErr?.message?.includes("Unauthorized");
if (sseIs401) {
const authError = new Error("Authentication required");
authError.code = 401;
throw authError;
}
throw new Error(
"Could not connect to server with any available transport"
);
}
}
}
async connectWithStreamableHttp(baseUrl) {
try {
this.connectionManager = new StreamableHttpConnectionManager(baseUrl, {
authProvider: this.opts.authProvider,
// ← Pass OAuth provider to SDK
requestInit: {
headers: this.headers
},
// Pass through timeout and other options
reconnectionOptions: {
maxReconnectionDelay: 3e4,
initialReconnectionDelay: 1e3,
reconnectionDelayGrowFactor: 1.5,
maxRetries: 2
}
});
const transport = await this.connectionManager.start();
this.client = new Client(this.clientInfo, this.opts.clientOptions);
try {
await this.client.connect(transport);
} catch (connectErr) {
if (connectErr instanceof Error) {
const errMsg = connectErr.message || connectErr.toString();
if (errMsg.includes("Missing session ID") || errMsg.includes("Bad Request: Missing session ID")) {
const wrappedError = new Error(
`FastMCP session ID error: ${errMsg}`
);
wrappedError.cause = connectErr;
throw wrappedError;
}
}
throw connectErr;
}
this.connected = true;
this.transportType = "streamable-http";
logger.debug(
`Successfully connected to MCP implementation via streamable HTTP: ${baseUrl}`
);
} catch (err) {
await this.cleanupResources();
throw err;
}
}
async connectWithSse(baseUrl) {
try {
this.connectionManager = new SseConnectionManager(baseUrl, {
requestInit: {
headers: this.headers
}
});
const transport = await this.connectionManager.start();
this.client = new Client(this.clientInfo, this.opts.clientOptions);
await this.client.connect(transport);
this.connected = true;
this.transportType = "sse";
logger.debug(
`Successfully connected to MCP implementation via HTTP/SSE: ${baseUrl}`
);
} catch (err) {
await this.cleanupResources();
throw err;
}
}
get publicIdentifier() {
return {
type: "http",
url: this.baseUrl,
transport: this.transportType || "unknown"
};
}
/**
* Get the transport type being used (streamable-http or sse)
*/
getTransportType() {
return this.transportType;
}
};
// src/connectors/websocket.ts
import { v4 as uuidv4 } from "uuid";
// src/task_managers/websocket.ts
import WS from "ws";
var WebSocketConnectionManager = class extends ConnectionManager {
static {
__name(this, "WebSocketConnectionManager");
}
url;
headers;
_ws = null;
/**
* @param url The WebSocket URL to connect to.
* @param headers Optional headers to include in the connection handshake.
*/
constructor(url, headers = {}) {
super();
this.url = url;
this.headers = headers;
}
/** Establish a WebSocket connection and wait until it is open. */
async establishConnection() {
logger.debug(`Connecting to WebSocket: ${this.url}`);
return new Promise((resolve, reject) => {
const ws = new WS(this.url, {
headers: this.headers
});
this._ws = ws;
const onOpen = /* @__PURE__ */ __name(() => {
cleanup();
logger.debug("WebSocket connected successfully");
resolve(ws);
}, "onOpen");
const onError = /* @__PURE__ */ __name((err) => {
cleanup();
logger.error(`Failed to connect to WebSocket: ${err}`);
reject(err);
}, "onError");
const cleanup = /* @__PURE__ */ __name(() => {
ws.off("open", onOpen);
ws.off("error", onError);
}, "cleanup");
ws.on("open", onOpen);
ws.on("error", onError);
});
}
/** Cleanly close the WebSocket connection. */
async closeConnection(connection) {
logger.debug("Closing WebSocket connection");
return new Promise((resolve) => {
const onClose = /* @__PURE__ */ __name(() => {
connection.off("close", onClose);
this._ws = null;
resolve();
}, "onClose");
if (connection.readyState === WS.CLOSED) {
onClose();
return;
}
connection.on("close", onClose);
try {
connection.close();
} catch (e) {
logger.warn(`Error closing WebSocket connection: ${e}`);
onClose();
}
});
}
};
// src/connectors/websocket.ts
var WebSocketConnector = class extends BaseConnector {
static {
__name(this, "WebSocketConnector");
}
url;
headers;
connectionManager = null;
ws = null;
receiverTask = null;
pending = /* @__PURE__ */ new Map();
toolsCache = null;
constructor(url, opts = {}) {
super();
this.url = url;
this.headers = { ...opts.headers ?? {} };
if (opts.authToken) this.headers.Authorization = `Bearer ${opts.authToken}`;
}
async connect() {
if (this.connected) {
logger.debug("Already connected to MCP implementation");
return;
}
logger.debug(`Connecting via WebSocket: ${this.url}`);
try {
this.connectionManager = new WebSocketConnectionManager(
this.url,
this.headers
);
this.ws = await this.connectionManager.start();
this.receiverTask = this.receiveLoop();
this.connected = true;
logger.debug("WebSocket connected successfully");
} catch (e) {
logger.error(`Failed to connect: ${e}`);
await this.cleanupResources();
throw e;
}
}
async disconnect() {
if (!this.connected) {
logger.debug("Not connected to MCP implementation");
return;
}
logger.debug("Disconnecting \u2026");
await this.cleanupResources();
this.connected = false;
}
sendRequest(method, params = null) {
if (!this.ws) throw new Error("WebSocket is not connected");
const id = uuidv4();
const payload = JSON.stringify({ id, method, params: params ?? {} });
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.ws.send(payload, (err) => {
if (err) {
this.pending.delete(id);
reject(err);
}
});
});
}
async receiveLoop() {
if (!this.ws) return;
const socket = this.ws;
const onMessage = /* @__PURE__ */ __name((msg) => {
let data;
try {
data = JSON.parse(msg.data ?? msg);
} catch (e) {
logger.warn("Received non\u2011JSON frame", e);
return;
}
const id = data.id;
if (id && this.pending.has(id)) {
const { resolve, reject } = this.pending.get(id);
this.pending.delete(id);
if ("result" in data) resolve(data.result);
else if ("error" in data) reject(data.error);
} else {
logger.debug("Received unsolicited message", data);
}
}, "onMessage");
if (socket.addEventListener) {
socket.addEventListener("message", onMessage);
} else {
socket.on("message", onMessage);
}
return new Promise((resolve) => {
const onClose = /* @__PURE__ */ __name(() => {
if (socket.removeEventListener) {
socket.removeEventListener("message", onMessage);
} else {
socket.off("message", onMessage);
}
this.rejectAll(new Error("WebSocket closed"));
resolve();
}, "onClose");
if (socket.addEventListener) {
socket.addEventListener("close", onClose);
} else {
socket.on("close", onClose);
}
});
}
rejectAll(err) {
for (const { reject } of this.pending.values()) reject(err);
this.pending.clear();
}
async initialize() {
logger.debug("Initializing MCP session over WebSocket");
const result = await this.sendRequest("initialize");
const toolsList = await this.listTools();
this.toolsCache = toolsList.map((t) => t);
logger.debug(`Initialized with ${this.toolsCache.length} tools`);
return result;
}
async listTools() {
const res = await this.sendRequest("tools/list");
return res.tools ?? [];
}
async callTool(name, args) {
return await this.sendRequest("tools/call", { name, arguments: args });
}
async listResources() {
const resources = await this.sendRequest("resources/list");
return { resources: Array.isArray(resources) ? resources : [] };
}
async readResource(uri) {
const res = await this.sendRequest("resources/read", { uri });
return res;
}
async request(method, params = null) {
return await this.sendRequest(method, params);
}
get tools() {
if (!this.toolsCache) throw new Error("MCP client is not initialized");
return this.toolsCache;
}
async cleanupResources() {
if (this.receiverTask) await this.receiverTask.catch(() => {
});
this.receiverTask = null;
this.rejectAll(new Error("WebSocket disconnected"));
if (this.connectionManager) {
await this.connectionManager.stop();
this.connectionManager = null;
this.ws = null;
}
this.toolsCache = null;
}
get publicIdentifier() {
return {
type: "websocket",
url: this.url
};
}
};
// src/auth/browser-provider.ts
import { sanitizeUrl } from "strict-url-sanitise";
var BrowserOAuthClientProvider = class {
static {
__name(this, "BrowserOAuthClientProvider");
}
serverUrl;
storageKeyPrefix;
serverUrlHash;
clientName;
clientUri;
callbackUrl;
preventAutoAuth;
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.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
}
};
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) {
if (this.preventAutoAuth) return;
const sanitizedAuthUrl = await this.prepareAuthorizationUrl(authorizationUrl);
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
import { auth } from "@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 auth(provider, {
serverUrl: baseUrl,
authorizationCode: code
});
if (authResult === "AUTHORIZED") {
console.log(
`${logPrefix} Authorization successful via SDK auth(). Notifying opener...`
);
if (window.opener && !window.opener.closed) {
window.opener.postMessage(
{ type: "mcp_auth_callback", success: true },
window.location.origin
);
window.close();
} else {
console.warn(
`${logPrefix} No opener window detected. Redirecting to root.`
);
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 || "/";
}
localStorage.removeItem(stateKey);
} 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 = `
<div style="font-family: sans-serif; padding: 20px;">
<h1>Authentication Error</h1>
<p style="color: red; background-color: #ffebeb; border: 1px solid red; padding: 10px; border-radius: 4px;">
${errorMessage}
</p>
<p>You can close this window or <a href="#" onclick="window.close(); return false;">click here to close</a>.</p>
<pre style="font-size: 0.8em; color: #555; margin-top: 20px; white-space: pre-wrap;">${err instanceof Error ? err.stack : ""}</pre>
</div>
`;
} 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");
// src/client/base.ts
var BaseMCPClient = class {
static {
__name(this, "BaseMCPClient");
}
config = {};
sessions = {};
activeSessions = [];
constructor(config) {
if (config) {
this.config = config;
}
}
static fromDict(_cfg) {
throw new Error("fromDict must be implemented by concrete class");
}
addServer(name, serverConfig) {
this.config.mcpServers = this.config.mcpServers || {};
this.config.mcpServers[name] = serverConfig;
}
removeServer(name) {
if (this.config.mcpServers?.[name]) {
delete this.config.mcpServers[name];
this.activeSessions = this.activeSessions.filter((n) => n !== name);
}
}
getServerNames() {
return Object.keys(this.config.mcpServers ?? {});
}
getServerConfig(name) {
return this.config.mcpServers?.[name];
}
getConfig() {
return this.config ?? {};
}
async createSession(serverName, autoInitialize = true) {
const servers = this.config.mcpServers ?? {};
if (Object.keys(servers).length === 0) {
logger.warn("No MCP servers defined in config");
}
if (!servers[serverName]) {
throw new Error(`Server '${serverName}' not found in config`);
}
const connector = this.createConnectorFromConfig(servers[serverName]);
const session = new MCPSession(connector);
if (autoInitialize) {
await session.initialize();
}
this.sessions[serverName] = session;
if (!this.activeSessions.includes(serverName)) {
this.activeSessions.push(serverName);
}
return session;
}
async createAllSessions(autoInitialize = true) {
const servers = this.config.mcpServers ?? {};
if (Object.keys(servers).length === 0) {
logger.warn("No MCP servers defined in config");
}
for (const name of Object.keys(servers)) {
await this.createSession(name, autoInitialize);
}
return this.sessions;
}
getSession(serverName) {
const session = this.sessions[serverName];
if (!session) {
return null;
}
return session;
}
getAllActiveSessions() {
return Object.fromEntries(
this.activeSessions.map((n) => [n, this.sessions[n]])
);
}
async closeSession(serverName) {
const session = this.sessions[serverName];
if (!session) {
logger.warn(
`No session exists for server ${serverName}, nothing to close`
);
return;
}
try {
logger.debug(`Closing session for server ${serverName}`);
await session.disconnect();
} catch (e) {
logger.error(`Error closing session for server '${serverName}': ${e}`);
} finally {
delete this.sessions[serverName];
this.activeSessions = this.activeSessions.filter((n) => n !== serverName);
}
}
async closeAllSessions() {
const serverNames = Object.keys(this.sessions);
const errors = [];
for (const serverName of serverNames) {
try {
logger.debug(`Closing session for server ${serverName}`);
await this.closeSession(serverName);
} catch (e) {
const errorMsg = `Failed to close session for server '${serverName}': ${e}`;
logger.error(errorMsg);
errors.push(errorMsg);
}
}
if (errors.length) {
logger.error(
`Encountered ${errors.length} errors while closing sessions`
);
} else {
logger.debug("All sessions closed successfully");
}
}
};
// src/client/browser.ts
var BrowserMCPClient = class _BrowserMCPClient extends BaseMCPClient {
static {
__name(this, "BrowserMCPClient");
}
constructor(config) {
super(config);
}
static fromDict(cfg) {
return new _BrowserMCPClient(cfg);
}
/**
* Create a connector from server configuration (Browser version)
* Supports HTTP and WebSocket connectors only
*/
createConnectorFromConfig(serverConfig) {
const { url, transport, headers, authToken, authProvider } = serverConfig;
if (!url) {
throw new Error("Server URL is required");
}
const connectorOptions = {
headers,
authToken,
authProvider
// ← Pass OAuth provider to connector
};
if (transport === "websocket" || url.startsWith("ws://") || url.startsWith("wss://")) {
return new WebSocketConnector(url, connectorOptions);
} else if (transport === "http" || url.startsWith("http://") || url.startsWith("https://")) {
return new HttpConnector(url, connectorOptions);
} else {
return new HttpConnector(url, connectorOptions);
}
}
};
export {
MCPSession,
BaseMCPClient,
ConnectionManager,
BaseConnector,
HttpConnector,
WebSocketConnector,
BrowserOAuthClientProvider,
onMcpAuthorization,
BrowserMCPClient
};