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.
2,609 lines • 95.3 kB
JavaScript
import {
applyProxyConfig,
detectFavicon
} from "./chunk-4PQK35NP.js";
import {
BrowserMCPClient
} from "./chunk-NSUUIFWS.js";
import {
Tel,
getPackageVersion
} from "./chunk-IBYLEFBS.js";
import {
BrowserOAuthClientProvider,
sanitizeUrl
} from "./chunk-M2YN2FFM.js";
import {
__name
} from "./chunk-3GQAWCBQ.js";
// src/react/useMcp.ts
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
// src/utils/assert.ts
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
__name(assert, "assert");
// src/react/useMcp.ts
var DEFAULT_RECONNECT_DELAY = 3e3;
var DEFAULT_RETRY_DELAY = 5e3;
function useMcp(options) {
const {
url,
enabled = true,
callbackUrl = typeof window !== "undefined" ? sanitizeUrl(
new URL("/oauth/callback", window.location.origin).toString()
) : "/oauth/callback",
storageKeyPrefix = "mcp:auth",
clientConfig = {},
customHeaders = {},
proxyConfig,
debug: _debug = false,
autoRetry = false,
autoReconnect = DEFAULT_RECONNECT_DELAY,
transportType = "auto",
preventAutoAuth = false,
// Default to false for backward compatibility (auto-trigger OAuth)
useRedirectFlow = false,
// Default to false for backward compatibility (use popup)
onPopupWindow,
timeout = 3e4,
// 30 seconds default for connection timeout
sseReadTimeout = 3e5,
// 5 minutes default for SSE read timeout
wrapTransport,
onNotification,
samplingCallback,
onElicitation
} = options;
const { url: finalUrl, headers: proxyHeaders } = useMemo(
() => applyProxyConfig(url || "", proxyConfig),
[url, proxyConfig]
);
const allHeaders = useMemo(
() => ({ ...proxyHeaders, ...customHeaders }),
[proxyHeaders, customHeaders]
);
const [state, setState] = useState("discovering");
const [tools, setTools] = useState([]);
const [resources, setResources] = useState([]);
const [resourceTemplates, setResourceTemplates] = useState([]);
const [prompts, setPrompts] = useState([]);
const [serverInfo, setServerInfo] = useState();
const [capabilities, setCapabilities] = useState();
const [error, setError] = useState(void 0);
const [log, setLog] = useState([]);
const [authUrl, setAuthUrl] = useState(void 0);
const [authTokens, setAuthTokens] = useState(void 0);
const clientRef = useRef(null);
const authProviderRef = useRef(null);
const iconLoadingPromiseRef = useRef(null);
const connectingRef = useRef(false);
const isMountedRef = useRef(true);
const connectAttemptRef = useRef(0);
const authTimeoutRef = useRef(null);
const retryScheduledRef = useRef(false);
const stateRef = useRef(state);
const autoReconnectRef = useRef(autoReconnect);
const successfulTransportRef = useRef(null);
const connectRef = useRef(null);
const failConnectionRef = useRef(null);
useEffect(() => {
stateRef.current = state;
autoReconnectRef.current = autoReconnect;
}, [state, autoReconnect]);
const addLog = useCallback(
(level, message, ...args) => {
const fullMessage = args.length > 0 ? `${message} ${args.map((arg) => JSON.stringify(arg)).join(" ")}` : message;
console[level](`[useMcp] ${fullMessage}`);
if (isMountedRef.current) {
setLog((prevLog) => [
...prevLog.slice(-100),
{ level, message: fullMessage, timestamp: Date.now() }
]);
}
},
[]
);
const disconnect = useCallback(
async (quiet = false) => {
if (!quiet) addLog("info", "Disconnecting...");
connectingRef.current = false;
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current);
authTimeoutRef.current = null;
if (clientRef.current) {
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (session && session._healthCheckCleanup) {
session._healthCheckCleanup();
session._healthCheckCleanup = null;
}
await clientRef.current.closeSession(serverName);
} catch (err) {
if (!quiet) addLog("warn", "Error closing session:", err);
}
}
clientRef.current = null;
if (isMountedRef.current && !quiet) {
setState("discovering");
setTools([]);
setResources([]);
setResourceTemplates([]);
setPrompts([]);
setError(void 0);
setAuthUrl(void 0);
}
},
[addLog]
);
const failConnection = useCallback(
(errorMessage, connectionError) => {
addLog("error", errorMessage, connectionError ?? "");
if (isMountedRef.current) {
setState("failed");
setError(errorMessage);
const manualUrl = authProviderRef.current?.getLastAttemptedAuthUrl();
if (manualUrl) {
setAuthUrl(manualUrl);
addLog(
"info",
"Manual authentication URL may be available.",
manualUrl
);
}
}
connectingRef.current = false;
if (url) {
Tel.getInstance().trackUseMcpConnection({
url,
transportType,
success: false,
errorType: connectionError?.name || "UnknownError",
hasOAuth: !!authProviderRef.current,
hasSampling: !!samplingCallback,
hasElicitation: !!onElicitation
}).catch(() => {
});
}
},
[addLog, url, transportType, samplingCallback, onElicitation]
);
const connect = useCallback(async () => {
if (!enabled || !url) {
addLog(
"debug",
enabled ? "No server URL provided, skipping connection." : "Connection disabled via enabled flag."
);
return;
}
if (connectingRef.current) {
addLog("debug", "Connection attempt already in progress.");
return;
}
if (!isMountedRef.current) {
addLog("debug", "Connect called after unmount, aborting.");
return;
}
connectingRef.current = true;
connectAttemptRef.current += 1;
setError(void 0);
setAuthUrl(void 0);
successfulTransportRef.current = null;
setState("discovering");
addLog(
"info",
`Connecting attempt #${connectAttemptRef.current} to ${url}...`
);
if (!authProviderRef.current) {
authProviderRef.current = new BrowserOAuthClientProvider(url, {
storageKeyPrefix,
clientName: clientConfig.name,
clientUri: clientConfig.uri,
logoUri: clientConfig.logo_uri || "https://mcp-use.com/logo.png",
callbackUrl,
preventAutoAuth,
useRedirectFlow,
onPopupWindow
});
addLog("debug", "BrowserOAuthClientProvider initialized in connect.");
}
if (!clientRef.current) {
clientRef.current = new BrowserMCPClient();
addLog("debug", "BrowserMCPClient initialized in connect.");
}
const tryConnectWithTransport = /* @__PURE__ */ __name(async (transportTypeParam, isAuthRetry = false) => {
addLog(
"info",
`Attempting connection with transport: ${transportTypeParam}`
);
try {
const serverName = "inspector-server";
const serverConfig = {
url: finalUrl,
transport: transportTypeParam === "sse" ? "http" : transportTypeParam,
// Disable SSE fallback when using explicit HTTP transport (not SSE)
// This prevents automatic HTTP → SSE fallback at the connector level
disableSseFallback: transportTypeParam === "http",
// Use SSE transport when explicitly requested
preferSse: transportTypeParam === "sse",
clientInfo: {
name: "mcp-use Inspector",
version: getPackageVersion()
}
};
if (allHeaders && Object.keys(allHeaders).length > 0) {
serverConfig.headers = allHeaders;
}
if (authProviderRef.current) {
const tokens = await authProviderRef.current.tokens();
if (tokens?.access_token) {
serverConfig.headers = {
...serverConfig.headers,
Authorization: `Bearer ${tokens.access_token}`
};
}
}
clientRef.current.addServer(serverName, {
...serverConfig,
authProvider: authProviderRef.current,
// ← SDK handles OAuth automatically!
clientOptions: clientConfig,
// ← Pass client config to connector
samplingCallback,
// ← Pass sampling callback to connector
elicitationCallback: onElicitation,
// ← Pass elicitation callback to connector
wrapTransport: wrapTransport ? (transport) => {
console.log(
"[useMcp] Applying transport wrapper for server:",
serverName,
"url:",
url
);
return wrapTransport(transport, url);
} : void 0
});
const session = await clientRef.current.createSession(
serverName,
false
);
session.on("notification", (notification) => {
onNotification?.(notification);
if (notification.method === "notifications/tools/list_changed") {
addLog("info", "Tools list changed, auto-refreshing...");
refreshTools().catch(
(err) => addLog("warn", "Auto-refresh tools failed:", err)
);
} else if (notification.method === "notifications/resources/list_changed") {
addLog("info", "Resources list changed, auto-refreshing...");
refreshResources().catch(
(err) => addLog("warn", "Auto-refresh resources failed:", err)
);
} else if (notification.method === "notifications/prompts/list_changed") {
addLog("info", "Prompts list changed, auto-refreshing...");
refreshPrompts().catch(
(err) => addLog("warn", "Auto-refresh prompts failed:", err)
);
}
});
await session.initialize();
addLog("info", "\u2705 Successfully connected to MCP server");
addLog("info", "Server info:", session.connector.serverInfo);
addLog(
"info",
"Server capabilities:",
session.connector.serverCapabilities
);
console.log("[useMcp] Server info:", session.connector.serverInfo);
console.log(
"[useMcp] Server capabilities:",
session.connector.serverCapabilities
);
setState("ready");
successfulTransportRef.current = transportTypeParam;
const setupConnectionMonitoring = /* @__PURE__ */ __name(() => {
let healthCheckInterval = null;
let lastSuccessfulCheck = Date.now();
const HEALTH_CHECK_INTERVAL = 1e4;
const HEALTH_CHECK_TIMEOUT = 3e4;
const checkConnectionHealth = /* @__PURE__ */ __name(async () => {
if (!isMountedRef.current || stateRef.current !== "ready") {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
healthCheckInterval = null;
}
return;
}
try {
await session.connector.listTools();
lastSuccessfulCheck = Date.now();
} catch (err) {
const timeSinceLastSuccess = Date.now() - lastSuccessfulCheck;
if (timeSinceLastSuccess > HEALTH_CHECK_TIMEOUT) {
addLog(
"warn",
`Connection appears to be broken (no response for ${Math.round(timeSinceLastSuccess / 1e3)}s), attempting to reconnect...`
);
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
healthCheckInterval = null;
}
if (autoReconnectRef.current && isMountedRef.current) {
setState("discovering");
addLog("info", "Auto-reconnecting to MCP server...");
setTimeout(
() => {
if (isMountedRef.current && stateRef.current === "discovering") {
connect();
}
},
typeof autoReconnectRef.current === "number" ? autoReconnectRef.current : DEFAULT_RECONNECT_DELAY
);
}
}
}
}, "checkConnectionHealth");
healthCheckInterval = setInterval(
checkConnectionHealth,
HEALTH_CHECK_INTERVAL
);
return () => {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
healthCheckInterval = null;
}
};
}, "setupConnectionMonitoring");
if (autoReconnect) {
const cleanup = setupConnectionMonitoring();
session._healthCheckCleanup = cleanup;
}
Tel.getInstance().trackUseMcpConnection({
url,
transportType: transportTypeParam,
success: true,
hasOAuth: !!authProviderRef.current,
hasSampling: !!samplingCallback,
hasElicitation: !!onElicitation
}).catch(() => {
});
setTools(session.connector.tools || []);
const resourcesResult = await session.connector.listAllResources();
setResources(resourcesResult.resources || []);
const promptsResult = await session.connector.listPrompts();
setPrompts(promptsResult.prompts || []);
const serverInfo2 = session.connector.serverInfo;
const capabilities2 = session.connector.serverCapabilities;
if (serverInfo2) {
console.log("[useMcp] Server info:", serverInfo2);
setServerInfo(serverInfo2);
const loadIconPromise = (async () => {
try {
const serverIcons = serverInfo2.icons;
if (serverIcons && Array.isArray(serverIcons) && serverIcons.length > 0) {
const iconUrl = serverIcons[0].src || serverIcons[0].url;
if (iconUrl) {
addLog("info", "Server provided icon:", iconUrl);
const res = await fetch(iconUrl);
const blob = await res.blob();
const base64 = await new Promise(
(resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}
);
if (isMountedRef.current) {
setServerInfo(
(prev) => prev ? { ...prev, icon: base64 } : void 0
);
addLog("debug", "Server icon converted to base64");
}
return base64;
}
}
if (url) {
const faviconBase64 = await detectFavicon(url);
if (faviconBase64 && isMountedRef.current) {
setServerInfo(
(prev) => prev ? { ...prev, icon: faviconBase64 } : void 0
);
addLog("debug", "Favicon detected and added to serverInfo");
return faviconBase64;
}
}
return null;
} catch (err) {
addLog("debug", "Icon loading failed (non-critical):", err);
return null;
}
})();
iconLoadingPromiseRef.current = loadIconPromise;
}
if (capabilities2) {
console.log("[useMcp] Server capabilities:", capabilities2);
setCapabilities(capabilities2);
}
if (authProviderRef.current) {
const tokens = await authProviderRef.current.tokens();
if (tokens?.access_token) {
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
setAuthTokens({
access_token: tokens.access_token,
token_type: tokens.token_type || "Bearer",
expires_at: expiresAt,
refresh_token: tokens.refresh_token,
scope: tokens.scope
});
}
}
return "success";
} catch (err) {
const error2 = err;
const errorMessage = error2?.message || String(err);
if (error2.code === 401 || errorMessage.includes("401") || errorMessage.includes("Unauthorized")) {
if (authProviderRef.current) {
addLog(
"info",
"Authentication required. OAuth provider available."
);
try {
const { auth } = await import("@modelcontextprotocol/sdk/client/auth.js");
const baseUrl = new URL(url).origin;
auth(authProviderRef.current, { serverUrl: baseUrl }).catch(
() => {
}
);
setTimeout(() => {
if (isMountedRef.current) {
const manualUrl = authProviderRef.current?.getLastAttemptedAuthUrl();
if (manualUrl) {
setAuthUrl(manualUrl);
addLog(
"info",
"Manual authentication URL available:",
manualUrl
);
} else {
addLog("warn", "Could not generate authentication URL");
}
}
}, 100);
} catch (authGenError) {
addLog("warn", "Error generating auth URL:", authGenError);
}
if (isMountedRef.current) {
setState("pending_auth");
}
connectingRef.current = false;
return "auth_redirect";
}
if (customHeaders && Object.keys(customHeaders).length > 0) {
failConnection(
"Authentication failed: Server returned 401 Unauthorized. Check your Authorization header value is correct."
);
return "failed";
}
failConnection(
"Authentication required: Server returned 401 Unauthorized. Add an Authorization header in the Custom Headers section (e.g., Authorization: Bearer YOUR_API_KEY)."
);
return "failed";
}
failConnection(
errorMessage,
error2 instanceof Error ? error2 : new Error(String(error2))
);
return "failed";
}
}, "tryConnectWithTransport");
let finalStatus = "failed";
if (transportType === "sse") {
addLog("debug", "Using SSE-only transport mode");
finalStatus = await tryConnectWithTransport("sse");
} else if (transportType === "http") {
addLog("debug", "Using HTTP-only transport mode");
finalStatus = await tryConnectWithTransport("http");
} else {
addLog("debug", "Using auto transport mode (HTTP with SSE fallback)");
const httpResult = await tryConnectWithTransport("http");
if (httpResult === "fallback" && isMountedRef.current && stateRef.current !== "authenticating") {
addLog("info", "HTTP failed, attempting SSE fallback...");
const sseResult = await tryConnectWithTransport("sse");
finalStatus = sseResult;
} else {
finalStatus = httpResult;
}
}
if (finalStatus === "success" || finalStatus === "failed" || finalStatus === "auth_redirect") {
connectingRef.current = false;
}
addLog("debug", `Connection sequence finished with status: ${finalStatus}`);
}, [
addLog,
failConnection,
disconnect,
url,
storageKeyPrefix,
callbackUrl,
clientConfig.name,
clientConfig.version,
clientConfig.uri,
clientConfig.logo_uri,
customHeaders,
transportType,
preventAutoAuth,
useRedirectFlow,
onPopupWindow,
enabled,
timeout,
sseReadTimeout
]);
useEffect(() => {
connectRef.current = connect;
failConnectionRef.current = failConnection;
}, [connect, failConnection]);
const callTool = useCallback(
async (name, args, options2) => {
if (stateRef.current !== "ready" || !clientRef.current) {
throw new Error(
`MCP client is not ready (current state: ${state}). Cannot call tool "${name}".`
);
}
addLog("info", `Calling tool: ${name}`, args);
const startTime = Date.now();
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (!session) {
throw new Error("No active session found");
}
const result = await session.connector.callTool(
name,
args || {},
options2
);
addLog("info", `Tool "${name}" call successful:`, result);
Tel.getInstance().trackUseMcpToolCall({
toolName: name,
success: true,
executionTimeMs: Date.now() - startTime
}).catch(() => {
});
return result;
} catch (err) {
addLog("error", `Tool "${name}" call failed:`, err);
Tel.getInstance().trackUseMcpToolCall({
toolName: name,
success: false,
errorType: err instanceof Error ? err.name : "UnknownError",
executionTimeMs: Date.now() - startTime
}).catch(() => {
});
throw err;
}
},
[state]
);
const retry = useCallback(() => {
if (stateRef.current === "failed") {
addLog("info", "Retry requested...");
connectRef.current?.();
} else {
addLog(
"warn",
`Retry called but state is not 'failed' (state: ${stateRef.current}). Ignoring.`
);
}
}, [addLog]);
const authenticate = useCallback(async () => {
addLog("info", "Manual authentication requested...");
const currentState = stateRef.current;
if (currentState === "failed") {
addLog("info", "Attempting to reconnect and authenticate via retry...");
retry();
} else if (currentState === "pending_auth") {
addLog("info", "Proceeding with authentication from pending state...");
try {
assert(
authProviderRef.current,
"Auth Provider not available for manual auth"
);
assert(url, "Server URL is required for authentication");
addLog("info", "Clearing all OAuth state and initiating fresh flow...");
const hashPrefix = `${storageKeyPrefix}:${authProviderRef.current.serverUrlHash}`;
Object.keys(localStorage).forEach((key) => {
if (key.startsWith(hashPrefix)) {
addLog("debug", `Removing stale OAuth key: ${key}`);
localStorage.removeItem(key);
}
if (key.startsWith(`${storageKeyPrefix}:state_`)) {
addLog("debug", `Removing orphaned state: ${key}`);
localStorage.removeItem(key);
}
});
setState("authenticating");
const freshAuthProvider = new BrowserOAuthClientProvider(url, {
storageKeyPrefix,
clientName: clientConfig.name,
clientUri: clientConfig.uri,
logoUri: clientConfig.logo_uri || "https://mcp-use.com/logo.png",
callbackUrl,
preventAutoAuth: false,
// ← Allow OAuth to proceed
useRedirectFlow,
onPopupWindow
});
authProviderRef.current = freshAuthProvider;
addLog("info", "Triggering fresh OAuth authorization...");
const { auth } = await import("@modelcontextprotocol/sdk/client/auth.js");
const baseUrl = new URL(url).origin;
auth(freshAuthProvider, {
serverUrl: baseUrl
}).catch((err) => {
addLog(
"info",
"OAuth flow initiated:",
err instanceof Error ? err.message : "Redirecting..."
);
});
} catch (authError) {
if (!isMountedRef.current) return;
setState("pending_auth");
addLog(
"error",
`Manual authentication failed: ${authError instanceof Error ? authError.message : String(authError)}`
);
}
} else if (currentState === "authenticating") {
addLog(
"warn",
"Already attempting authentication. Check for blocked popups or wait for timeout."
);
const manualUrl = authProviderRef.current?.getLastAttemptedAuthUrl();
if (manualUrl && !authUrl) {
setAuthUrl(manualUrl);
addLog("info", "Manual authentication URL retrieved:", manualUrl);
}
} else {
addLog(
"info",
`Client not in a state requiring manual authentication trigger (state: ${currentState}). If needed, try disconnecting and reconnecting.`
);
}
}, [
addLog,
retry,
authUrl,
url,
useRedirectFlow,
onPopupWindow,
storageKeyPrefix,
clientConfig.name,
clientConfig.uri,
clientConfig.logo_uri,
callbackUrl
]);
const clearStorage = useCallback(() => {
if (authProviderRef.current) {
const count = authProviderRef.current.clearStorage();
addLog("info", `Cleared ${count} item(s) from localStorage for ${url}.`);
setAuthUrl(void 0);
disconnect();
} else {
addLog("warn", "Auth provider not initialized, cannot clear storage.");
}
}, [url, addLog, disconnect]);
const listResources = useCallback(async () => {
if (stateRef.current !== "ready" || !clientRef.current) {
throw new Error(
`MCP client is not ready (current state: ${state}). Cannot list resources.`
);
}
addLog("info", "Listing resources");
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (!session) {
throw new Error("No active session found");
}
const resourcesResult = await session.connector.listAllResources();
setResources(resourcesResult.resources || []);
addLog("info", "Resources listed successfully");
} catch (err) {
addLog("error", "List resources failed:", err);
throw err;
}
}, [state]);
const readResource = useCallback(
async (uri) => {
if (stateRef.current !== "ready" || !clientRef.current) {
throw new Error(
`MCP client is not ready (current state: ${state}). Cannot read resource.`
);
}
addLog("info", `Reading resource: ${uri}`);
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (!session) {
throw new Error("No active session found");
}
const result = await session.connector.readResource(uri);
addLog("info", "Resource read successful:", result);
Tel.getInstance().trackUseMcpResourceRead({
resourceUri: uri,
success: true
}).catch(() => {
});
return result;
} catch (err) {
addLog("error", "Resource read failed:", err);
Tel.getInstance().trackUseMcpResourceRead({
resourceUri: uri,
success: false,
errorType: err instanceof Error ? err.name : "UnknownError"
}).catch(() => {
});
throw err;
}
},
[state]
);
const listPrompts = useCallback(async () => {
if (stateRef.current !== "ready" || !clientRef.current) {
throw new Error(
`MCP client is not ready (current state: ${state}). Cannot list prompts.`
);
}
addLog("info", "Listing prompts");
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (!session) {
throw new Error("No active session found");
}
const promptsResult = await session.connector.listPrompts();
setPrompts(promptsResult.prompts || []);
addLog("info", "Prompts listed successfully");
} catch (err) {
addLog("error", "List prompts failed:", err);
throw err;
}
}, [state]);
const refreshTools = useCallback(async () => {
if (stateRef.current !== "ready" || !clientRef.current) {
addLog("debug", "Cannot refresh tools - client not ready");
return;
}
addLog("debug", "Refreshing tools list");
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (!session) {
addLog("warn", "No active session found for tools refresh");
return;
}
const toolsResult = await session.connector.listTools();
setTools(toolsResult || []);
addLog("info", "Tools list refreshed successfully");
} catch (err) {
addLog("warn", "Failed to refresh tools:", err);
}
}, [addLog]);
const refreshResources = useCallback(async () => {
if (stateRef.current !== "ready" || !clientRef.current) {
addLog("debug", "Cannot refresh resources - client not ready");
return;
}
addLog("debug", "Refreshing resources list");
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (!session) {
addLog("warn", "No active session found for resources refresh");
return;
}
const resourcesResult = await session.connector.listAllResources();
setResources(resourcesResult.resources || []);
addLog("info", "Resources list refreshed successfully");
} catch (err) {
addLog("warn", "Failed to refresh resources:", err);
}
}, [addLog]);
const refreshPrompts = useCallback(async () => {
if (stateRef.current !== "ready" || !clientRef.current) {
addLog("debug", "Cannot refresh prompts - client not ready");
return;
}
addLog("debug", "Refreshing prompts list");
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (!session) {
addLog("warn", "No active session found for prompts refresh");
return;
}
const promptsResult = await session.connector.listPrompts();
setPrompts(promptsResult.prompts || []);
addLog("info", "Prompts list refreshed successfully");
} catch (err) {
addLog("warn", "Failed to refresh prompts:", err);
}
}, [addLog]);
const refreshAll = useCallback(async () => {
addLog("info", "Refreshing all lists (tools, resources, prompts)");
await Promise.all([refreshTools(), refreshResources(), refreshPrompts()]);
}, [refreshTools, refreshResources, refreshPrompts, addLog]);
const getPrompt = useCallback(
async (name, args) => {
if (stateRef.current !== "ready" || !clientRef.current) {
throw new Error(
`MCP client is not ready (current state: ${state}). Cannot get prompt.`
);
}
addLog("info", `Getting prompt: ${name}`, args);
try {
const serverName = "inspector-server";
const session = clientRef.current.getSession(serverName);
if (!session) {
throw new Error("No active session found");
}
const result = await session.connector.getPrompt(name, args || {});
addLog("info", `Prompt "${name}" retrieved successfully:`, result);
return result;
} catch (err) {
addLog("error", `Prompt "${name}" retrieval failed:`, err);
throw err;
}
},
[state]
);
useEffect(() => {
const messageHandler = /* @__PURE__ */ __name((event) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type === "mcp_auth_callback") {
addLog("info", "Received auth callback message.", event.data);
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current);
authTimeoutRef.current = null;
if (event.data.success) {
addLog(
"info",
"Authentication successful via popup. Reconnecting client..."
);
if (connectingRef.current) {
addLog(
"debug",
"Connection attempt already in progress, resetting flag to allow reconnection."
);
}
connectingRef.current = false;
setTimeout(() => {
if (isMountedRef.current) {
addLog(
"debug",
"Initiating reconnection after successful auth callback."
);
connectRef.current?.();
}
}, 100);
} else {
failConnectionRef.current?.(
`Authentication failed in callback: ${event.data.error || "Unknown reason."}`
);
}
}
}, "messageHandler");
window.addEventListener("message", messageHandler);
addLog("debug", "Auth callback message listener added.");
return () => {
window.removeEventListener("message", messageHandler);
addLog("debug", "Auth callback message listener removed.");
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current);
};
}, [addLog]);
useEffect(() => {
isMountedRef.current = true;
if (!enabled || !url) {
addLog(
"debug",
enabled ? "No server URL provided, skipping connection." : "Connection disabled via enabled flag."
);
setState("discovering");
return () => {
isMountedRef.current = false;
};
}
addLog("debug", "useMcp mounted, initiating connection.");
connectAttemptRef.current = 0;
if (!authProviderRef.current || authProviderRef.current.serverUrl !== url) {
authProviderRef.current = new BrowserOAuthClientProvider(url, {
storageKeyPrefix,
clientName: clientConfig.name,
clientUri: clientConfig.uri,
logoUri: clientConfig.logo_uri || "https://mcp-use.com/logo.png",
callbackUrl,
preventAutoAuth,
useRedirectFlow,
onPopupWindow
});
addLog(
"debug",
"BrowserOAuthClientProvider initialized/updated on mount/option change."
);
}
connect();
return () => {
isMountedRef.current = false;
addLog("debug", "useMcp unmounting, disconnecting.");
disconnect(true);
};
}, [
url,
enabled,
storageKeyPrefix,
callbackUrl,
clientConfig.name,
clientConfig.version,
clientConfig.uri,
clientConfig.logo_uri,
useRedirectFlow
]);
const retryRef = useRef(retry);
const addLogRef = useRef(addLog);
useEffect(() => {
retryRef.current = retry;
addLogRef.current = addLog;
}, [retry, addLog]);
useEffect(() => {
let retryTimeoutId = null;
if (state === "failed" && autoRetry && connectAttemptRef.current > 0) {
if (!retryScheduledRef.current) {
retryScheduledRef.current = true;
const delay = typeof autoRetry === "number" ? autoRetry : DEFAULT_RETRY_DELAY;
addLogRef.current(
"info",
`Connection failed, auto-retrying in ${delay}ms...`
);
retryTimeoutId = setTimeout(() => {
retryScheduledRef.current = false;
if (isMountedRef.current && stateRef.current === "failed") {
retryRef.current();
}
}, delay);
}
} else if (state !== "failed") {
retryScheduledRef.current = false;
}
return () => {
if (retryTimeoutId) {
clearTimeout(retryTimeoutId);
retryScheduledRef.current = false;
}
};
}, [state, autoRetry]);
const ensureIconLoaded = useCallback(async () => {
if (stateRef.current !== "ready") {
addLog("warn", "Cannot ensure icon loaded - not connected");
return null;
}
if (serverInfo?.icon) {
return serverInfo.icon;
}
if (iconLoadingPromiseRef.current) {
addLog("debug", "Waiting for icon to finish loading...");
const icon = await iconLoadingPromiseRef.current;
return icon;
}
addLog("debug", "No icon available and no loading in progress");
return null;
}, [serverInfo, addLog]);
return {
state,
name: serverInfo?.name || url || "",
tools,
resources,
resourceTemplates,
prompts,
serverInfo,
capabilities,
error,
log,
authUrl,
authTokens,
client: clientRef.current,
callTool,
readResource,
listResources,
listPrompts,
getPrompt,
refreshTools,
refreshResources,
refreshPrompts,
refreshAll,
retry,
disconnect,
authenticate,
clearStorage,
ensureIconLoaded
};
}
__name(useMcp, "useMcp");
// src/react/ErrorBoundary.tsx
import React from "react";
var ErrorBoundary = class extends React.Component {
static {
__name(this, "ErrorBoundary");
}
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("Widget Error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return /* @__PURE__ */ React.createElement("div", { className: "p-4 border border-red-500 bg-red-50 text-red-900 rounded-md dark:bg-red-900/20 dark:text-red-100" }, /* @__PURE__ */ React.createElement("h3", { className: "font-bold mb-2" }, "Widget Error"), /* @__PURE__ */ React.createElement("pre", { className: "text-sm whitespace-pre-wrap" }, this.state.error?.message));
}
return this.props.children;
}
};
// src/react/Image.tsx
import React2 from "react";
var Image = /* @__PURE__ */ __name(({ src, ...props }) => {
const publicUrl = typeof window !== "undefined" ? window.__mcpPublicAssetsUrl || window.__mcpPublicUrl || "" : "";
const getFinalSrc = /* @__PURE__ */ __name((source) => {
if (!source) return source;
if (source.startsWith("http://") || source.startsWith("https://") || source.startsWith("data:")) {
return source;
}
if (!publicUrl) {
return source;
}
const cleanSrc = source.startsWith("/") ? source.slice(1) : source;
return `${publicUrl}/${cleanSrc}`;
}, "getFinalSrc");
const finalSrc = getFinalSrc(src);
return /* @__PURE__ */ React2.createElement("img", { src: finalSrc, ...props });
}, "Image");
// src/react/ThemeProvider.tsx
import React3, { useEffect as useEffect3, useLayoutEffect, useState as useState3 } from "react";
// src/react/useWidget.ts
import {
useCallback as useCallback2,
useEffect as useEffect2,
useMemo as useMemo2,
useState as useState2,
useSyncExternalStore
} from "react";
// src/react/widget-types.ts
var SET_GLOBALS_EVENT_TYPE = "openai:set_globals";
// src/react/useWidget.ts
function useOpenAiGlobal(key) {
return useSyncExternalStore(
(onChange) => {
const handleSetGlobal = /* @__PURE__ */ __name((event) => {
const customEvent = event;
const value = customEvent.detail.globals[key];
if (value === void 0) {
return;
}
onChange();
}, "handleSetGlobal");
if (typeof window !== "undefined") {
window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
}
return () => {
if (typeof window !== "undefined") {
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
}
};
},
() => typeof window !== "undefined" && window.openai ? window.openai[key] : void 0
);
}
__name(useOpenAiGlobal, "useOpenAiGlobal");
function useWidget(defaultProps) {
const [isOpenAiAvailable, setIsOpenAiAvailable] = useState2(
() => typeof window !== "undefined" && !!window.openai
);
useEffect2(() => {
if (typeof window !== "undefined" && window.openai) {
setIsOpenAiAvailable(true);
return;
}
const checkInterval = setInterval(() => {
if (typeof window !== "undefined" && window.openai) {
setIsOpenAiAvailable(true);
clearInterval(checkInterval);
}
}, 100);
const handleSetGlobals = /* @__PURE__ */ __name(() => {
if (typeof window !== "undefined" && window.openai) {
setIsOpenAiAvailable(true);
clearInterval(checkInterval);
}
}, "handleSetGlobals");
if (typeof window !== "undefined") {
window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobals);
}
const timeout = setTimeout(() => {
clearInterval(checkInterval);
if (typeof window !== "undefined") {
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobals);
}
}, 5e3);
return () => {
clearInterval(checkInterval);
clearTimeout(timeout);
if (typeof window !== "undefined") {
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobals);
}
};
}, []);
const provider = useMemo2(() => {
return isOpenAiAvailable ? "openai" : "mcp-ui";
}, [isOpenAiAvailable]);
const searchString = typeof window !== "undefined" ? window.location.search : "";
const urlParams = useMemo2(() => {
const urlParams2 = new URLSearchParams(searchString);
if (urlParams2.has("mcpUseParams")) {
return JSON.parse(urlParams2.get("mcpUseParams"));
}
return {
toolInput: {},
toolOutput: {},
toolId: ""
};
}, [searchString]);
const toolInput = provider === "openai" ? useOpenAiGlobal("toolInput") : urlParams.toolInput;
const toolOutput = provider === "openai" ? useOpenAiGlobal("toolOutput") : urlParams.toolOutput;
const toolResponseMetadata = useOpenAiGlobal("toolResponseMetadata");
const widgetProps = useMemo2(() => {
if (toolResponseMetadata && typeof toolResponseMetadata === "object") {
const metaProps = toolResponseMetadata["mcp-use/props"];
if (metaProps) {
return metaProps;
}
}
return defaultProps || {};
}, [toolResponseMetadata, defaultProps]);
const widgetState = useOpenAiGlobal("widgetState");
const theme = useOpenAiGlobal("theme");
const displayMode = useOpenAiGlobal("displayMode");
const safeArea = useOpenAiGlobal("safeArea");
const maxHeight = useOpenAiGlobal("maxHeight");
const userAgent = useOpenAiGlobal("userAgent");
const locale = useOpenAiGlobal("locale");
const mcp_url = useMemo2(() => {
if (typeof window !== "undefined" && window.__mcpPublicUrl) {
return window.__mcpPublicUrl.replace(/\/mcp-use\/public$/, "");
}
return "";
}, []);
const [localWidgetState, setLocalWidgetState] = useState2(null);
useEffect2(() => {
if (widgetState !== void 0) {
setLocalWidgetState(widgetState);
}
}, [widgetState]);
const callTool = useCallback2(
async (name, args) => {
if (!window.openai?.callTool) {
throw new Error("window.openai.callTool is not available");
}
return window.openai.callTool(name, args);
},
[]
);
const sendFollowUpMessage = useCallback2(
async (prompt) => {
if (!window.openai?.sendFollowUpMessage) {
throw new Error("window.openai.sendFollowUpMessage is not available");
}
return window.openai.sendFollowUpMessage({ prompt });
},
[]
);
const openExternal = useCallback2((href) => {
if (!window.openai?.openExternal) {
throw new Error("window.openai.openExternal is not available");
}
window.openai.openExternal({ href });
}, []);
const requestDisplayMode = useCallback2(
async (mode) => {
if (!window.openai?.requestDisplayMode) {
throw new Error("window.openai.requestDisplayMode is not available");
}
return window.openai.requestDisplayMode({ mode });
},
[]
);
const setState = useCallback2(
async (state) => {
if (!window.openai?.setWidgetState) {
throw new Error("window.openai.setWidgetState is not available");
}
const currentState = widgetState !== void 0 ? widgetState : localWidgetState;
const newState = typeof state === "function" ? state(currentState) : state;
setLocalWidgetState(newState);
return window.openai.setWidgetState(newState);
},
[widgetState, localWidgetState]
);
const isPending = useMemo2(() => {
return provider === "openai" && toolResponseMetadata === null;
}, [provider, toolResponseMetadata]);
return {
// Props and state (with defaults)
props: widgetProps,
toolInput: toolInput || {},
output: toolOutput ?? null,
metadata: toolResponseMetadata ?? null,
state: localWidgetState,
setState,
// Layout and theme (with safe defaults)
theme: theme || "light",
displayMode: displayMode || "inline",
safeArea: safeArea || { insets: { top: 0, bottom: 0, left: 0, right: 0 } },
maxHeight: maxHeight || 600,
userAgent: userAgent || {
device: { type: "desktop" },
capabilities: { hover: true, touch: false }
},
locale: locale || "en",
mcp_url,
// Actions
callTool,
sendFollowUpMessage,
openExternal,
requestDisplayMode,
// Availability
isAvailable: isOpenAiAvailable,
isPending
};
}
__name(useWidget, "useWidget");
function useWidgetProps(defaultProps) {
const { props } = useWidget(defaultProps);
return props;
}
__name(useWidgetProps, "useWidgetProps");
function useWidgetTheme() {
const { theme } = useWidget();
return theme;
}
__name(useWidgetTheme, "useWidgetTheme");
function useWidgetState(defaultState) {
const { state, setState } = useWidget();
useEffect2(() => {
if (state === null && defaultState !== void 0 && window.openai?.setWidgetState) {
setState(defaultState);
}
}, []);
return [state, setState];
}
__name(useWidgetState, "useWidgetState");
// src/react/ThemeProvider.tsx
var ThemeProvider = /* @__PURE__ */ __name(({
children
}) => {
const { theme, isAvailable } = useWidget();
console.log("theme", theme);
const [systemPreference, setSystemPreference] = useState3(
() => {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
);
useEffect3(() => {
if (typeof window === "undefined") return;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = /* @__PURE__ */ __name((e) => {
setSystemPreference(e.matches ? "dark" : "light");
}, "handleChange");
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, []);
const effectiveTheme = isAvailable ? theme : systemPreference;
useLayoutEffect(() => {
if (typeof document === "undefined") return;
if (effectiveTheme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}, [effectiveTheme]);
return /* @__PURE__ */ React3.createElement(React3.Fragment, null, children);
}, "ThemeProvider");
// src/react/WidgetControls.tsx
import React4, { useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
function WidgetControls({
children,
className = "",
position = "top-right",
attachTo,
showLabels = true,
debugger: enableDebugger = false,
viewControls = false
}) {
const {
props,
output,
metadata,
state,
theme,
displayMode,
safeArea,
maxHeight,
userAgent,
locale,
isAvailable,
callTool,
sendFollowUpMessage,
openExternal,
requestDisplayMode,
setState
} = useWidget();
const [isHovered, setIsHovered] = useState4(false);
const [isOverlayOpen, setIsOverlayOpen] = useState4(false);
const containerRef = useRef2(null);
const overlayRef = useRef2(null);
const [windowOpenAiKeys, setWindowOpenAiKeys] = useState4([]);
const [actionResult, setActionResult] = useState4("");
const [toolName, setToolName] = useState4("get-my-city");
const [toolArgs, setToolArgs] = useState4("{}");
const [followUpMessage, setFollowUpMessage] = useState4(
"Test follow-up message"
);
const [externalUrl, setExternalUrl] = useState4(
"https://docs.mcp-use.com"
);
const isFullscreen = displayMode === "fullscreen" && isAvailable;
const isPip = displayMode === "pip" && isAvailable;
const isInInspector = typeof window !== "undefined" && window.location.pathname.includes("/inspector/api/");
useEffect4(() => {
const timeoutId = setTimeout(() => {
if (typeof window !== "undefined" && window.openai) {
try {
const keys = Object.keys(window.openai);
setWindowOpenAiKeys(keys);
} catch (e) {
setWindowOpenAiKeys([]);
}
} else {
setWindowOpenAiKeys([]);
}
}, 100);
return () => {
clearTimeout(timeoutId);
};
}, []);
const isDark = theme === "dark";
const getPositionClasses = /* @__PURE__ */ __name(() => {
const baseClasses = [
"absolute",
"z-[1000]",
"flex",
"gap-2",
"transition-opacity",
"duration-200",
"ease-in-out",
isHovered ? "opacity-100" : "opacity-0",
isHovered ? "pointer-events-auto" : "pointer-events-none"
];
switch (position) {
case "top-left":
return [...baseClasses, "top-4", "left-4"];
case "top-center":
return [...baseClasses, "top-4", "left-1/2", "-translate-x-1/2"];
case "top-right":
return [...baseClasses, "top-4", "right-4"];
case "center-left":
return [...baseClasses, "top-1/2", "left-4", "-translate-y-1/2"];
case "center-right":
return [...baseClasses, "top-1/2", "right-4", "-translate-y-1/2"];
case "bottom-left":
return [...baseClasses, "bottom-4", "left-4"];
case "bottom-center":
return [...baseClasses, "bottom-4", "left-1/2", "-translate-x-1/2"];
case "bottom-right":
return [...baseClasses, "bottom-4", "right-4"];
default:
return [...baseClasses, "top-4", "right-4"];
}
}, "getPositionClasses");
const getPositionOffsetStyles = /* @__PURE__ */ __name(() => {
const baseOffset = 16;
const topOffset = safeArea?.insets?.top ? Math.max(baseOffset, safeArea.insets.top + 8) : baseOffset;
const rightOffset = safeArea?.insets?.right ? Math.max(baseOffset, safeArea.insets.right + 8) : baseOffset;
const bottomOffset = safeArea?.insets?.bottom ? Math.max(baseOffset, safeArea.insets.bottom + 8) : baseOffset;
const leftOffset = safeArea?.insets?.left ? Math.max(baseOffset, safeArea.insets.left + 8) : baseOffset;
const styles = {};
switch (position) {
case "top-left":
styles.top = `${topOffset}px`;
styles.left = `${leftOffset}px`;
break;
case "top-center":
styles.top = `${topOffset}px`;
break;
case "top-right":
styles.top = `${topOffset}px`;
styles.right = `${rightOffset}px`;
break;
case "center-left":
styles.left = `${leftOffset}px`;
break;
case "center-right":
styles.right = `${rightOffset}px`;
break;
case "bottom-left":
styles.bottom = `${bottomOffset}px`;
styles.left = `${leftOffset}px`;
break;
case "bottom-center":
styles.bottom = `${bottomOffset}px`;
break;
case "bottom-right":
styles.bottom = `${bottomOffset}px`;
styles.right = `${rightOffset}px`;
break;
default:
styles.top = `${topOffset}px`;
styles.right = `${rightOffset}px`;
break;
}
return styles;
}, "getPositionOffsetStyles");
useEffect4(() => {
if (!attachTo) return;
const handleMouseEnter = /* @__PURE__ */ __name(() => setIsHovered(true), "handleMouseEnter");
const handleMouseLeave = /* @__PURE__ */ __name(() => setIsHovered(false), "handleMouseLeave");
attachTo.addEventListener("mouseenter", handleMouseEnter);
attachTo.addEventListener("mouseleave", handleMouseLeave);
return () => {
attachTo.removeEventListener("mouseenter", handleMouseEnter);
attachTo.removeEventListener("mouseleave", handleMouseLeave);
};
}, [attachTo]);
useEffect4(() => {
if (!isOverlayOpen) return;
const handleClickOutside = /* @__PURE__ */ __name((event) => {
if (overlayRef.current && !overlayRef.current.contains(event.target)) {
setIsOverlayOpen(false);
}
}, "handleClickOutside");
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen]);
useEffect4(() => {
if (isOverlayOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOverlayOpen]);
const handleToggleOverlay = /* @__PURE__ */ __name(() => {
setIsOverlayOpen(!isOverlayOpen);
}, "handleToggleOverlay");
const handleCallTool = /* @__PURE__ */ __name(async () => {
try {
setActionResult("Calling tool...");
const args = toolArgs.trim() ? JSON.parse(toolArgs) : {};
const result = await callTool(toolName, args);
setActionResult(`Success: ${JSON.stringify(result, null, 2)}`);
} catch (error) {
const err = error;
setActionResult(`Error: ${err.message}`);
}
}, "handleCallTool");
const handleSendFollowUpMessage = /* @__PURE__ */ __name(async () => {
try {
setActionResult("Sending follow-up message...");
await sendFollowUpMessage(followUpMessage);
setActionResult("Follow-up message sent successfully");
} catch (error) {
const err = error;
setActionResult(`Error: ${err.message}`);
}
}, "handleSendFollowUpMessage");
const handleOpenExternal = /* @__PURE__ */ __name(() => {
try {
openExternal(externalUrl);
setActionResult(`Opened external link: ${externalUrl}`);
} catch (error) {
const err = error;
setActionResult(`Error: ${err.message}`);
}
}, "handleOpenExternal");
const handleRequestDisplayMode = /* @__PURE__ */ __name(async (mode) => {
try {
setActionResult(`Requesting display mode: ${mode}...`);
const result = await requestDisplayMode(mode);
setActionResult(`Display mode granted: ${result.mode}`);
} catch (error) {
const err = error;
setActionResult(`Error: ${err.message}`);
}
}, "handleRequestDisplayMode");
const handleSetState = /* @__PURE__ */ __name(async () => {
try {
const newState = state ? { ...state, debugTimestamp: (/* @__PURE__ */ new Date()).toISOString() } : { debugTimestamp: (/* @__PURE__ */ new Date()).toISOString() };
setActionResult("Setting state...");
await setState(newState);
setActionResult(`State updated: ${JSON.stringify(newState, null, 2)}`);
} catch (error) {
const err = error;
setActionResult(`Error: ${err.message}`);
}
}, "handleSetState");
const handleFullscreen = /* @__PURE__ */ __name(async () => {
try {
await requestDisplayMode("fullscreen");
} catch (error) {
console.error("Failed to go fullscreen:", error);
}
}, "handleFullscreen");
const handlePip = /* @__PURE__ */ __name(async () => {
try {
await requestDisplayMode("pip");
} catch (error) {
console.error("Failed to go pip:", error);
}
}, "handlePip");
const getTooltipClasses = /* @__PURE__ */ __name(() => {
const baseClasses = [
"absolute",
"px-2",
"py-1",
"bg-black/90",
"text-white",
"rounded",
"text-xs",
"whitespace-nowrap",
"pointer-events-none",
"transition-opacity",
"duration-200",
"ease-in-out"
];
switch (position) {
case "top-right":
return [...baseClasses, "top-full", "right-0", "mt-2"];
case "top-left":
return [...baseClasses, "top-full", "left-0", "mt-2"];
case "top-center":
return [
...baseClasses,
"top-full",
"left-1/2",
"-translate-x-1/2",
"mt-2"
];
case "bottom-right":
return [...baseClasses, "bottom-full", "right-0", "mb-2"];
case "bottom-left":
return [...baseClasses, "bottom-full", "left-0", "mb-2"];
case "bottom-center":
return [
...baseClasses,
"bottom-full",
"left-1/2",
"-translate-x-1/2",
"mb-2"
];
case "center-left":
return [
...baseClasses,
"left-full",
"top-1/2",
"-translate-y-1/2",
"ml-2"
];
case "center-right":
return [
...baseClasses,
"right-full",
"top-1/2",
"-translate-y-1/2",
"mr-2"
];
default:
return [...baseClasses, "top-full", "right-0", "mt-2"];
}
}, "getTooltipClasses");
const IconButton = /* @__PURE__ */ __name(({
onClick,
label,
children: icon
}) => {
const [isButtonHovered, setIsButtonHovered] = useState4(false);
const tooltipClasses = getTooltipClasses();
return /* @__PURE__ */ React4.createElement(
"button",
{
className: `p-2 ${isDark ? "bg-white/10 hover:bg-white/20" : "bg-black/70 hover:bg-black/90"} text-white border-none rounded-lg cursor-pointer flex items-center justify-center w-8 h-8 transition-colors duration-200 backdrop-blur-md ${isDark ? "shadow-[0_2px_8px_rgba(0,0,0,0.3)]" : "shadow-[0_2px_8px_rgba(0,0,0,0.2)]"} relative`,
onMouseEnter: () => setIsButtonHovered(true),
onMouseLeave: () => setIsButtonHovered(false),
onClick,
"aria-label": label
},
/* @__PURE__ */ React4.createElement(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
className: "block"
},
icon
),
showLabels && /* @__PURE__ */ React4.createElement(
"span",
{
className: `${tooltipClasses.join(" ")} ${isButtonHovered ? "opacity-100" : "opacity-0"}`
},
label
)
);
}, "IconButton");
const formatValue = /* @__PURE__ */ __name((value) => {
if (value === null) return "null";
if (value === void 0) return "undefined";
if (typeof value === "object") {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
return String(value);
}, "formatValue");
const formatUserAgent = /* @__PURE__ */ __name((ua) => {
if (!ua) return "N/A";
return `${ua.device?.type || "unknown"}`;
}, "formatUserAgent");
const formatSafeArea = /* @__PURE__ */ __name((sa) => {
if (!sa?.insets) return "N/A";
const { top, bottom, left, right } = sa.insets;
return `T:${top} B:${bottom} L:${left} R:${right}`;
}, "formatSafeArea");
return /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(
"div",
{
ref: containerRef,
className: `${className} relative h-fit`,
onMouseEnter: () => !attachTo && setIsHovered(true),
onMouseLeave: () => !attachTo && setIsHovered(false)
},
/* @__PURE__ */ React4.createElement(
"div",
{
className: getPositionClasses().join(" "),
style: getPositionOffsetStyles()
},
!isInInspector && /* @__PURE__ */ React4.createElement(React4.Fragment, null, !isFullscreen && !isPip && /* @__PURE__ */ React4.createElement(React4.Fragment, null, (viewControls === true || viewControls === "fullscreen") && /* @__PURE__ */ React4.createElement(IconButton, { onClick: handleFullscreen, label: "Fullscreen" }, /* @__PURE__ */ React4.createElement("path", { d: "M15 3h6v6" }), /* @__PURE__ */ React4.createElement("path", { d: "m21 3-7 7" }), /* @__PURE__ */ React4.createElement("path", { d: "m3 21 7-7" }), /* @__PURE__ */ React4.createElement("path", { d: "M9 21H3v-6" })), (viewControls === true || viewControls === "pip") && /* @__PURE__ */ React4.createElement(IconButton, { onClick: handlePip, label: "Picture in Picture" }, /* @__PURE__ */ React4.createElement("path", { d: "M21 9V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h4" }), /* @__PURE__ */ React4.createElement("rect", { width: "10", height: "7", x: "12", y: "13", rx: "2" }))), enableDebugger && /* @__PURE__ */ React4.createElement(IconButton, { onClick: handleToggleOverlay, label: "Debug Info" }, /* @__PURE__ */ React4.createElement("path", { d: "M12 20v-9" }), /* @__PURE__ */ React4.createElement("path", { d: "M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z" }), /* @__PURE__ */ React4.createElement("path", { d: "M14.12 3.88 16 2" }), /* @__PURE__ */ React4.createElement("path", { d: "M21 21a4 4 0 0 0-3.81-4" }), /* @__PURE__ */ React4.createElement("path", { d: "M21 5a4 4 0 0 1-3.55 3.97" }), /* @__PURE__ */ React4.createElement("path", { d: "M22 13h-4" }), /* @__PURE__ */ React4.createElement("path", { d: "M3 21a4 4 0 0 1 3.81-4" }), /* @__PURE__ */ React4.createElement("path", { d: "M3 5a4 4 0 0 0 3.55 3.97" }), /* @__PURE__ */ React4.createElement("path", { d: "M6 13H2" }), /* @__PURE__ */ React4.createElement("path", { d: "m8 2 1.88 1.88" }), /* @__PURE__ */ React4.createElement("path", { d: "M9 7.13V6a3 3 0 1 1 6 0v1.13" })))
),
children
), isOverlayOpen && enableDebugger && /* @__PURE__ */ React4.createElement(
"div",
{
ref: overlayRef,
className: "fixed inset-0 bg-black text-white font-mono text-xs z-[10000] overflow-auto p-4",
onClick: (e) => {
if (e.target === overlayRef.current) {
setIsOverlayOpen(false);
}
}
},
/* @__PURE__ */ React4.createElement(
"button",
{
onClick: () => setIsOverlayOpen(false),
className: "absolute top-4 right-4 bg-white/10 text-white border-none rounded w-8 h-8 cursor-pointer flex items-center justify-center text-lg leading-none",
"aria-label": "Close"
},
"\xD7"
),
/* @__PURE__ */ React4.createElement("div", { className: "max-w-[1200px] mx-auto pt-10" }, /* @__PURE__ */ React4.createElement("h1", { className: "text-lg font-bold mb-4 border-b border-gray-700 pb-2" }, "Debug Info"), /* @__PURE__ */ React4.createElement("table", { className: "w-full border-collapse border-spacing-0" }, /* @__PURE__ */ React4.createElement("tbody", null, /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "Props"), /* @__PURE__ */ React4.createElement("td", { className: "p-2 whitespace-pre-wrap break-all" }, formatValue(props))), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "Output"), /* @__PURE__ */ React4.createElement("td", { className: "p-2 whitespace-pre-wrap break-all" }, formatValue(output))), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "Metadata"), /* @__PURE__ */ React4.createElement("td", { className: "p-2 whitespace-pre-wrap break-all" }, formatValue(metadata))), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "State"), /* @__PURE__ */ React4.createElement("td", { className: "p-2 whitespace-pre-wrap break-all" }, formatValue(state))), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "Theme"), /* @__PURE__ */ React4.createElement("td", { className: "p-2" }, theme)), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "Display Mode"), /* @__PURE__ */ React4.createElement("td", { className: "p-2" }, displayMode)), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "Locale"), /* @__PURE__ */ React4.createElement("td", { className: "p-2" }, locale)), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "Max Height"), /* @__PURE__ */ React4.createElement("td", { className: "p-2" }, maxHeight, "px")), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "User Agent"), /* @__PURE__ */ React4.createElement("td", { className: "p-2" }, formatUserAgent(userAgent))), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "Safe Area"), /* @__PURE__ */ React4.createElement("td", { className: "p-2" }, formatSafeArea(safeArea))), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "API Available"), /* @__PURE__ */ React4.createElement("td", { className: "p-2" }, isAvailable ? "Yes" : "No")), /* @__PURE__ */ React4.createElement("tr", { className: "border-b border-gray-700" }, /* @__PURE__ */ React4.createElement("td", { className: "p-2 font-bold w-[200px] align-top" }, "window.openai Keys"), /* @__PURE__ */ React4.createElement("td", { className: "p-2" }, windowOpenAiKeys.length > 0 ? windowOpenAiKeys.join(", ") : "N/A")))), /* @__PURE__ */ React4.createElement("h2", { className: "text-base font-bold mt-8 mb-4 border-b border-gray-700 pb-2" }, "Actions"), /* @__PURE__ */ React4.createElement("div", { className: "flex flex-col gap-3" }, /* @__PURE__ */ React4.createElement("div", { className: "flex gap-2 items-center" }, /* @__PURE__ */ React4.createElement(
"input",
{
type: "text",
value: toolName,
onChange: (e) => setToolName(e.target.value),
placeholder: "Tool name",
className: "py-1.5 px-2 bg-[#1a1a1a] text-white border border-gray-700 rounded font-mono text-xs w-[150px]"
}
), /* @__PURE__ */ React4.createElement(
"input",
{
type: "text",
value: toolArgs,
onChange: (e) => setToolArgs(e.target.value),
placeholder: '{"key": "value"}',
className: "py-1.5 px-2 bg-[#1a1a1a] text-white border border-gray-700 rounded font-mono text-xs flex-1"
}
), /* @__PURE__ */ React4.createElement(
"button",
{
onClick: handleCallTool,
className: "py-1.5 px-3 bg-gray-800 text-white border border-gray-600 rounded cursor-pointer font-mono text-xs"
},
"Call Tool"
)), /* @__PURE__ */ React4.createElement("div", { className: "flex gap-2 items-center" }, /* @__PURE__ */ React4.createElement(
"input",
{
type: "text",
value: followUpMessage,
onChange: (e) => setFollowUpMessage(e.target.value),
placeholder: "Follow-up message",
className: "py-1.5 px-2 bg-[#1a1a1a] text-white border border-gray-700 rounded font-mono text-xs flex-1"
}
), /* @__PURE__ */ React4.createElement(
"button",
{
onClick: handleSendFollowUpMessage,
className: "py-1.5 px-3 bg-gray-800 text-white border border-gray-600 rounded cursor-pointer font-mono text-xs"
},
"Send Follow-Up"
)), /* @__PURE__ */ React4.createElement("div", { className: "flex gap-2 items-center" }, /* @__PURE__ */ React4.createElement(
"input",
{
type: "text",
value: externalUrl,
onChange: (e) => setExternalUrl(e.target.value),
placeholder: "External URL",
className: "py-1.5 px-2 bg-[#1a1a1a] text-white border border-gray-700 rounded font-mono text-xs flex-1"
}
), /* @__PURE__ */ React4.createElement(
"button",
{
onClick: handleOpenExternal,
className: "py-1.5 px-3 bg-gray-800 text-white border border-gray-600 rounded cursor-pointer font-mono text-xs"
},
"Open Link"
)), /* @__PURE__ */ React4.createElement("div", { className: "flex gap-2 items-center" }, /* @__PURE__ */ React4.createElement("span", { className: "w-[150px] text-xs" }, "Display Mode:"), /* @__PURE__ */ React4.createElement(
"button",
{
onClick: () => handleRequestDisplayMode("inline"),
className: "py-1.5 px-3 bg-gray-800 text-white border border-gray-600 rounded cursor-pointer font-mono text-xs flex-1"
},
"Inline"
), /* @__PURE__ */ React4.createElement(
"button",
{
onClick: () => handleRequestDisplayMode("pip"),
className: "py-1.5 px-3 bg-gray-800 text-white border border-gray-600 rounded cursor-pointer font-mono text-xs flex-1"
},
"PiP"
), /* @__PURE__ */ React4.createElement(
"button",
{
onClick: () => handleRequestDisplayMode("fullscreen"),
className: "py-1.5 px-3 bg-gray-800 text-white border border-gray-600 rounded cursor-pointer font-mono text-xs flex-1"
},
"Fullscreen"
)), /* @__PURE__ */ React4.createElement("div", { className: "flex gap-2 items-center" }, /* @__PURE__ */ React4.createElement(
"button",
{
onClick: handleSetState,
className: "py-1.5 px-3 bg-gray-800 text-white border border-gray-600 rounded cursor-pointer font-mono text-xs"
},
"Set State (Add Timestamp)"
)), actionResult && /* @__PURE__ */ React4.createElement("div", { className: "mt-2 p-2 bg-[#1a1a1a] border border-gray-700 rounded whitespace-pre-wrap break-all text-[11px] max-h-[200px] overflow-auto" }, /* @__PURE__ */ React4.createElement("div", { className: "font-bold mb-1 text-gray-400" }, "Result:"), actionResult, /* @__PURE__ */ React4.createElement(
"button",
{
onClick: () => setActionResult(""),
className: "mt-2 py-1 px-2 bg-gray-800 text-white border border-gray-600 rounded cursor-pointer font-mono text-[11px]"
},
"Clear"
))))
));
}
__name(WidgetControls, "WidgetControls");
// src/react/McpUseProvider.tsx
import React5, {
StrictMode,
useCallback as useCallback3,
useEffect as useEffect5,
useRef as useRef3,
useState as useState5
} from "react";
function getBasename() {
if (typeof window === "undefined") return "/";
const path = window.location.pathname;
const match = path.match(/^(\/inspector\/api\/dev-widget\/[^/]+)/);
if (match) {
return match[1];
}
return "/";
}
__name(getBasename, "getBasename");
var HEIGHT_DEBOUNCE_MS = 150;
var MIN_HEIGHT_CHANGE_PX = 5;
function McpUseProvider({
children,
debugger: enableDebugger = false,
viewControls = false,
autoSize = false
}) {
const basename = getBasename();
const containerRef = useRef3(null);
const lastHeightRef = useRef3(0);
const debounceTimeoutRef = useRef3(null);
const notificationInProgressRef = useRef3(false);
const [BrowserRouter, setBrowserRouter] = useState5(null);
const [routerError, setRouterError] = useState5(null);
const [isRouterLoading, setIsRouterLoading] = useState5(true);
useEffect5(() => {
let mounted = true;
(async () => {
try {
const routerModule = await import("react-router");
if (mounted) {
setBrowserRouter(() => routerModule.BrowserRouter);
setIsRouterLoading(false);
}
} catch (error) {
if (mounted) {
setRouterError(
new Error(
"\u274C react-router not installed!\n\nTo use MCP widgets with McpUseProvider, you need to install:\n\n npm install react-router\n # or\n pnpm add react-router\n\nThis dependency is automatically included in projects created with 'create-mcp-use-app'."
)
);
setIsRouterLoading(false);
}
}
})();
return () => {
mounted = false;
};
}, []);
const notifyHeight = useCallback3((height) => {
if (typeof window !== "undefined" && window.openai?.notifyIntrinsicHeight) {
notificationInProgressRef.current = true;
window.openai.notifyIntrinsicHeight(height).then(() => {
notificationInProgressRef.current = false;
}).catch((error) => {
notificationInProgressRef.current = false;
console.error(
"[McpUseProvider] Failed to notify intrinsic height:",
error
);
});
}
}, []);
const debouncedNotifyHeight = useCallback3(
(height) => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
debounceTimeoutRef.current = setTimeout(() => {
const heightDiff = Math.abs(height - lastHeightRef.current);
if (heightDiff >= MIN_HEIGHT_CHANGE_PX && height > 0) {
lastHeightRef.current = height;
notifyHeight(height);
}
}, HEIGHT_DEBOUNCE_MS);
},
[notifyHeight]
);
useEffect5(() => {
if (!autoSize) {
return;
}
const container = containerRef.current;
if (!container || typeof ResizeObserver === "undefined") {
return;
}
const observer = new ResizeObserver((entries) => {
if (notificationInProgressRef.current) {
return;
}
for (const entry of entries) {
const height = entry.contentRect.height;
const scrollHeight = entry.target.scrollHeight;
const intrinsicHeight = Math.max(height, scrollHeight);
debouncedNotifyHeight(intrinsicHeight);
}
});
observer.observe(container);
const initialHeight = Math.max(
container.offsetHeight,
container.scrollHeight
);
if (initialHeight > 0) {
debouncedNotifyHeight(initialHeight);
}
return () => {
observer.disconnect();
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
debounceTimeoutRef.current = null;
}
notificationInProgressRef.current = false;
};
}, [autoSize, debouncedNotifyHeight]);
if (isRouterLoading) {
return /* @__PURE__ */ React5.createElement(StrictMode, null, /* @__PURE__ */ React5.createElement(ThemeProvider, null, /* @__PURE__ */ React5.createElement("div", { style: { padding: "20px", textAlign: "center" } }, "Loading...")));
}
if (routerError) {
throw routerError;
}
let content = children;
content = /* @__PURE__ */ React5.createElement(ErrorBoundary, null, content);
if (enableDebugger || viewControls) {
content = /* @__PURE__ */ React5.createElement(WidgetControls, { debugger: enableDebugger, viewControls }, content);
}
if (BrowserRouter) {
content = /* @__PURE__ */ React5.createElement(BrowserRouter, { basename }, content);
}
content = /* @__PURE__ */ React5.createElement(ThemeProvider, null, content);
if (autoSize) {
const containerStyle = {
width: "100%",
minHeight: 0
};
content = /* @__PURE__ */ React5.createElement("div", { ref: containerRef, style: containerStyle }, content);
}
return /* @__PURE__ */ React5.createElement(StrictMode, null, content);
}
__name(McpUseProvider, "McpUseProvider");
// src/react/McpClientProvider.tsx
import React6, {
createContext,
useCallback as useCallback4,
useContext,
useEffect as useEffect6,
useMemo as useMemo3,
useRef as useRef4,
useState as useState6
} from "react";
var McpClientContext = createContext(null);
var MAX_NOTIFICATIONS = 500;
function McpServerWrapper({
id,
options,
onUpdate,
rpcWrapTransport,
onGlobalSamplingRequest,
onGlobalElicitationRequest
}) {
const {
name,
onSamplingRequest,
onElicitationRequest,
onNotificationReceived,
wrapTransport: optionsWrapTransport
} = options;
const mcpOptions = useMemo3(() => {
const {
name: _name,
onSamplingRequest: _onSamplingRequest,
onElicitationRequest: _onElicitationRequest,
onNotificationReceived: _onNotificationReceived,
wrapTransport: _wrapTransport,
...rest
} = options;
return rest;
}, [options]);
const combinedWrapTransport = useMemo3(() => {
if (!rpcWrapTransport && !optionsWrapTransport) return void 0;
return (transport) => {
let wrapped = transport;
if (rpcWrapTransport) {
wrapped = rpcWrapTransport(wrapped, id);
}
if (optionsWrapTransport) {
wrapped = optionsWrapTransport(wrapped, id);
}
return wrapped;
};
}, [rpcWrapTransport, optionsWrapTransport, id]);
const [notifications, setNotifications] = useState6([]);
const [pendingSamplingRequests, setPendingSamplingRequests] = useState6([]);
const samplingIdCounter = useRef4(0);
const [pendingElicitationRequests, setPendingElicitationRequests] = useState6([]);
const elicitationIdCounter = useRef4(0);
const markNotificationRead = useCallback4((notificationId) => {
setNotifications(
(prev) => prev.map((n) => n.id === notificationId ? { ...n, read: true } : n)
);
}, []);
const markAllNotificationsRead = useCallback4(() => {
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
}, []);
const clearNotifications = useCallback4(() => {
setNotifications([]);
}, []);
const handleNotification = useCallback4(
(notification) => {
const mcpNotification = {
id: globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`,
method: notification.method,
params: notification.params,
timestamp: Date.now(),
read: false
};
setNotifications((prev) => {
const updated = [mcpNotification, ...prev];
if (updated.length > MAX_NOTIFICATIONS) {
return updated.slice(0, MAX_NOTIFICATIONS);
}
return updated;
});
onNotificationReceived?.(mcpNotification);
},
[onNotificationReceived]
);
const approveSampling = useCallback4(
(requestId, result) => {
setPendingSamplingRequests((prev) => {
const request = prev.find((r) => r.id === requestId);
if (request) {
request.resolve(result);
return prev.filter((r) => r.id !== requestId);
}
return prev;
});
},
[]
);
const rejectSampling = useCallback4((requestId, error) => {
setPendingSamplingRequests((prev) => {
const request = prev.find((r) => r.id === requestId);
if (request) {
request.reject(new Error(error || "User rejected sampling request"));
return prev.filter((r) => r.id !== requestId);
}
return prev;
});
}, []);
const samplingCallback = useCallback4(
async (params) => {
return new Promise((resolve, reject) => {
const requestId = `sampling-${samplingIdCounter.current++}`;
const request = {
id: requestId,
request: { method: "sampling/createMessage", params },
timestamp: Date.now(),
serverName: name || id
};
const newRequest = {
...request,
resolve,
reject
};
setPendingSamplingRequests((prev) => [...prev, newRequest]);
onSamplingRequest?.(request);
onGlobalSamplingRequest?.(
request,
id,
name || id,
approveSampling,
rejectSampling
);
});
},
[
id,
name,
onSamplingRequest,
onGlobalSamplingRequest,
approveSampling,
rejectSampling
]
);
const approveElicitation = useCallback4(
(requestId, result) => {
setPendingElicitationRequests((prev) => {
const request = prev.find((r) => r.id === requestId);
if (request) {
request.resolve(result);
return prev.filter((r) => r.id !== requestId);
}
return prev;
});
},
[]
);
const rejectElicitation = useCallback4((requestId, error) => {
setPendingElicitationRequests((prev) => {
const request = prev.find((r) => r.id === requestId);
if (request) {
request.reject(new Error(error || "User rejected elicitation request"));
return prev.filter((r) => r.id !== requestId);
}
return prev;
});
}, []);
const elicitationCallback = useCallback4(
async (params) => {
return new Promise((resolve, reject) => {
const requestId = `elicitation-${elicitationIdCounter.current++}`;
const request = {
id: requestId,
request: params,
timestamp: Date.now(),
serverName: name || id
};
const newRequest = {
...request,
resolve,
reject
};
setPendingElicitationRequests((prev) => [...prev, newRequest]);
onElicitationRequest?.(request);
onGlobalElicitationRequest?.(
request,
id,
name || id,
approveElicitation,
rejectElicitation
);
});
},
[
id,
name,
onElicitationRequest,
onGlobalElicitationRequest,
approveElicitation,
rejectElicitation
]
);
const mcp = useMcp({
...mcpOptions,
onNotification: handleNotification,
samplingCallback,
onElicitation: elicitationCallback,
wrapTransport: combinedWrapTransport
});
const publicSamplingRequests = useMemo3(
() => pendingSamplingRequests.map((r) => ({
id: r.id,
request: r.request,
timestamp: r.timestamp,
serverName: r.serverName
})),
[pendingSamplingRequests]
);
const publicElicitationRequests = useMemo3(
() => pendingElicitationRequests.map((r) => ({
id: r.id,
request: r.request,
timestamp: r.timestamp,
serverName: r.serverName
})),
[pendingElicitationRequests]
);
const unreadNotificationCount = useMemo3(
() => notifications.filter((n) => !n.read).length,
[notifications]
);
const onUpdateRef = useRef4(onUpdate);
const prevServerRef = useRef4(null);
useEffect6(() => {
onUpdateRef.current = onUpdate;
}, [onUpdate]);
useEffect6(() => {
const server = {
...mcp,
id,
url: options.url || "",
name: name || id,
notifications,
unreadNotificationCount,
markNotificationRead,
markAllNotificationsRead,
clearNotifications,
pendingSamplingRequests: publicSamplingRequests,
approveSampling,
rejectSampling,
pendingElicitationRequests: publicElicitationRequests,
approveElicitation,
rejectElicitation
};
const prevServer = prevServerRef.current;
if (!prevServer || prevServer.state !== server.state || prevServer.error !== server.error || prevServer.authUrl !== server.authUrl || prevServer.tools.length !== server.tools.length || prevServer.resources.length !== server.resources.length || prevServer.prompts.length !== server.prompts.length || prevServer.serverInfo !== server.serverInfo || prevServer.capabilities !== server.capabilities || prevServer.notifications.length !== server.notifications.length || prevServer.unreadNotificationCount !== server.unreadNotificationCount || prevServer.pendingSamplingRequests.length !== server.pendingSamplingRequests.length || prevServer.pendingElicitationRequests.length !== server.pendingElicitationRequests.length || !prevServer.client) {
prevServerRef.current = server;
onUpdateRef.current(server);
}
}, [
id,
name,
options.url,
// Primitive values that indicate meaningful state changes
mcp.state,
mcp.error,
mcp.authUrl,
// Use array LENGTHS instead of array references to avoid triggering on reference changes
mcp.tools.length,
mcp.resources.length,
mcp.resourceTemplates.length,
mcp.prompts.length,
// Objects excluded - manual comparison handles serverInfo/capabilities changes
// Functions excluded - they're stable via useCallback in useMcp
// mcp.log excluded - log changes shouldn't trigger provider updates
// mcp.client excluded - client reference stability handled by manual check
notifications.length,
unreadNotificationCount,
publicSamplingRequests.length,
publicElicitationRequests.length,
// Callback functions are stable via useCallback
markNotificationRead,
markAllNotificationsRead,
clearNotifications,
approveSampling,
rejectSampling,
approveElicitation,
rejectElicitation
]);
return null;
}
__name(McpServerWrapper, "McpServerWrapper");
function McpClientProvider({
children,
mcpServers,
storageProvider,
enableRpcLogging = false,
onServerAdded,
onServerRemoved,
onServerStateChange,
onSamplingRequest,
onElicitationRequest
}) {
const [serverConfigs, setServerConfigs] = useState6([]);
const [servers, setServers] = useState6([]);
const [storageLoaded, setStorageLoaded] = useState6(false);
const [rpcWrapTransport, setRpcWrapTransport] = useState6(void 0);
const [rpcLoggingReady, setRpcLoggingReady] = useState6(false);
useEffect6(() => {
if (!enableRpcLogging || typeof window === "undefined") {
setRpcWrapTransport(void 0);
setRpcLoggingReady(true);
return;
}
import("./rpc-logger-INMU527Q.js").then((module) => {
console.log("[McpClientProvider] RPC logger loaded");
setRpcWrapTransport(() => module.wrapTransportForLogging);
setRpcLoggingReady(true);
}).catch((err) => {
console.error("[McpClientProvider] Failed to load RPC logger:", err);
setRpcWrapTransport(void 0);
setRpcLoggingReady(true);
});
}, [enableRpcLogging]);
useEffect6(() => {
if (!rpcLoggingReady) {
console.log(
"[McpClientProvider] Waiting for RPC logging to be ready before loading servers"
);
return;
}
const loadServers = /* @__PURE__ */ __name(async () => {
console.log(
"[McpClientProvider] Loading servers, storageProvider:",
!!storageProvider,
"mcpServers:",
mcpServers
);
if (!storageProvider) {
if (mcpServers) {
const configs = Object.entries(mcpServers).map(([id, options]) => ({
id,
options
}));
console.log(
"[McpClientProvider] Loaded from mcpServers prop:",
configs.length
);
setServerConfigs(configs);
}
setStorageLoaded(true);
return;
}
try {
const storedServers = await Promise.resolve(
storageProvider.getServers()
);
console.log(
"[McpClientProvider] Loaded from storage:",
Object.keys(storedServers).length
);
const mergedServers = { ...storedServers, ...mcpServers };
const configs = Object.entries(mergedServers).map(([id, options]) => ({
id,
options
}));
console.log(
"[McpClientProvider] Total servers after merge:",
configs.length
);
setServerConfigs(configs);
setStorageLoaded(true);
} catch (error) {
console.error(
"[McpClientProvider] Failed to load from storage:",
error
);
if (mcpServers) {
const configs = Object.entries(mcpServers).map(([id, options]) => ({
id,
options
}));
setServerConfigs(configs);
}
setStorageLoaded(true);
}
}, "loadServers");
loadServers();
}, [storageProvider, mcpServers, rpcLoggingReady]);
useEffect6(() => {
if (!storageProvider || !storageLoaded) return;
const saveServers = /* @__PURE__ */ __name(async () => {
try {
const serversToSave = serverConfigs.reduce(
(acc, config) => {
acc[config.id] = config.options;
return acc;
},
{}
);
await Promise.resolve(storageProvider.setServers(serversToSave));
} catch (error) {
console.error("[McpClientProvider] Failed to save to storage:", error);
}
}, "saveServers");
saveServers();
}, [serverConfigs, storageProvider, storageLoaded]);
const handleServerUpdate = useCallback4(
(updatedServer) => {
setServers((prev) => {
const index = prev.findIndex((s) => s.id === updatedServer.id);
const isNewServer = index === -1;
if (isNewServer) {
onServerAdded?.(updatedServer.id, updatedServer);
return [...prev, updatedServer];
}
const current = prev[index];
const stateChanged = current.state !== updatedServer.state;
if (current.state === updatedServer.state && current.tools === updatedServer.tools && current.resources === updatedServer.resources && current.prompts === updatedServer.prompts && current.error === updatedServer.error && current.serverInfo === updatedServer.serverInfo && current.client === updatedServer.client && current.notifications === updatedServer.notifications && current.unreadNotificationCount === updatedServer.unreadNotificationCount && current.pendingSamplingRequests.length === updatedServer.pendingSamplingRequests.length && current.pendingElicitationRequests.length === updatedServer.pendingElicitationRequests.length) {
return prev;
}
if (stateChanged) {
onServerStateChange?.(updatedServer.id, updatedServer.state);
}
const newServers = [...prev];
newServers[index] = updatedServer;
return newServers;
});
},
[onServerAdded, onServerStateChange]
);
const addServer = useCallback4((id, options) => {
console.log("[McpClientProvider] addServer called:", id, options);
setServerConfigs((prev) => {
if (prev.find((s) => s.id === id)) {
console.warn(
`[McpClientProvider] Server with id "${id}" already exists`
);
return prev;
}
console.log("[McpClientProvider] Adding new server to configs:", id);
return [...prev, { id, options }];
});
}, []);
const removeServer = useCallback4(
(id) => {
setServers((prev) => {
const server = prev.find((s) => s.id === id);
if (server?.disconnect) {
server.disconnect();
}
if (server?.clearStorage) {
server.clearStorage();
}
return prev.filter((s) => s.id !== id);
});
setServerConfigs((prev) => prev.filter((s) => s.id !== id));
onServerRemoved?.(id);
},
[onServerRemoved]
);
const updateServer = useCallback4(
async (id, options) => {
return new Promise((resolve) => {
const currentConfig = serverConfigs.find((s) => s.id === id);
if (!currentConfig) {
console.warn(
`[McpClientProvider] Cannot update server "${id}" - not found`
);
resolve();
return;
}
const updatedOptions = {
...currentConfig.options,
...options
};
setServers((prev) => {
const server = prev.find((s) => s.id === id);
if (server?.disconnect) {
server.disconnect();
}
if (server?.clearStorage) {
server.clearStorage();
}
return prev.filter((s) => s.id !== id);
});
setServerConfigs((prev) => {
const updated = prev.map(
(s) => s.id === id ? { id, options: updatedOptions } : s
);
setTimeout(() => resolve(), 0);
return updated;
});
});
},
[serverConfigs]
);
const getServer = useCallback4(
(id) => {
return servers.find((s) => s.id === id);
},
[servers]
);
const contextValue = useMemo3(
() => ({
servers,
addServer,
removeServer,
updateServer,
getServer,
storageLoaded
}),
[servers, addServer, removeServer, updateServer, getServer, storageLoaded]
);
return /* @__PURE__ */ React6.createElement(McpClientContext.Provider, { value: contextValue }, children, serverConfigs.map((config) => /* @__PURE__ */ React6.createElement(
McpServerWrapper,
{
key: config.id,
id: config.id,
options: config.options,
onUpdate: handleServerUpdate,
rpcWrapTransport,
onGlobalSamplingRequest: onSamplingRequest,
onGlobalElicitationRequest: onElicitationRequest
}
)));
}
__name(McpClientProvider, "McpClientProvider");
function useMcpClient() {
const context = useContext(McpClientContext);
if (!context) {
throw new Error("useMcpClient must be used within a McpClientProvider");
}
return context;
}
__name(useMcpClient, "useMcpClient");
function useMcpServer(id) {
const { getServer } = useMcpClient();
return getServer(id);
}
__name(useMcpServer, "useMcpServer");
// src/react/storage/LocalStorageProvider.ts
var LocalStorageProvider = class {
constructor(storageKey = "mcp-client-servers") {
this.storageKey = storageKey;
}
static {
__name(this, "LocalStorageProvider");
}
getServers() {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.error("[LocalStorageProvider] Failed to load servers:", error);
return {};
}
}
setServers(servers) {
try {
localStorage.setItem(this.storageKey, JSON.stringify(servers));
} catch (error) {
console.error("[LocalStorageProvider] Failed to save servers:", error);
}
}
setServer(id, config) {
const servers = this.getServers();
servers[id] = config;
this.setServers(servers);
}
removeServer(id) {
const servers = this.getServers();
delete servers[id];
this.setServers(servers);
}
clear() {
try {
localStorage.removeItem(this.storageKey);
} catch (error) {
console.error("[LocalStorageProvider] Failed to clear:", error);
}
}
};
// src/react/storage/MemoryStorageProvider.ts
var MemoryStorageProvider = class {
static {
__name(this, "MemoryStorageProvider");
}
storage = {};
getServers() {
return { ...this.storage };
}
setServers(servers) {
this.storage = { ...servers };
}
setServer(id, config) {
this.storage[id] = config;
}
removeServer(id) {
delete this.storage[id];
}
clear() {
this.storage = {};
}
};
export {
useMcp,
ErrorBoundary,
Image,
useWidget,
useWidgetProps,
useWidgetTheme,
useWidgetState,
ThemeProvider,
WidgetControls,
McpUseProvider,
McpClientProvider,
useMcpClient,
useMcpServer,
LocalStorageProvider,
MemoryStorageProvider
};