The Model Calling Protocol (MCP) has significantly improved the way AI assistants interact with external tools, allowing for more powerful and versatile applications. However, a key limitation exists within the standard MCP implementation: tool callings are essentially "black box" operations with no built-in user interface for interaction. This creates a disconnect in user experience, particularly in cases where visual feedback or user input would be beneficial during the tool execution process. In this article, I'll explore an innovative approach we've developed in 21st Magic MCP to overcome this limitation by creating a browser-based interface for MCP communications, specifically focused on UI component generation with our 21st.dev integration. 21st Magic MCP Problem: Limited User Interaction in MCP Model Calling Protocol allows AI assistants to invoke external tools to perform specialized tasks. While powerful, the standard implementation has a significant drawback: This restriction is particularly problematic when generating UI components. When an AI suggests a UI component, users often need to: See various design options Compare different implementations Customize details before integration Make informed choices based on visual representation See various design options See various design options Compare different implementations Compare different implementations Customize details before integration Customize details before integration Make informed choices based on visual representation Make informed choices based on visual representation The standard MCP approach offers no built-in mechanism for this kind of interactive feedback loop. Solution: Browser-Based MCP Communication To address this limitation, we've developed a system that enables communication with MCPs through a browser interface. This approach: Creates a local MCP that can host a bundle and open a web browser Serves a local bundle alongside the MCP in NPM Automatically opens a browser that redirects to a user interface Allows users to interact with and select from available options Shuts down the server and resumes execution with the user's selection Creates a local MCP that can host a bundle and open a web browser Serves a local bundle alongside the MCP in NPM Automatically opens a browser that redirects to a user interface Allows users to interact with and select from available options Shuts down the server and resumes execution with the user's selection The result is a seamless integration that maintains the power of MCP while adding the visual feedback and interaction capabilities users need. Technical implementation Let's look at how this is implemented. Callback Server At the core of our solution is a callback server that facilitates communication between the MCP and the browser interface: export class CallbackServer { private server: Server | null = null; private port: number; private sessionId = Math.random().toString(36).substring(7); // ... other properties async promptUser( config: CallbackServerConfig = {} ): Promise<CallbackResponse> { const { initialData = null, timeout = 300000 } = config; this.config = config; try { const availablePort = await this.findAvailablePort(); this.server = createServer(this.handleRequest); this.server.listen(availablePort, "127.0.0.1"); // Set up promise to handle user selection return new Promise<CallbackResponse>((resolve, reject) => { this.promiseResolve = resolve; this.promiseReject = reject; // ... server setup code // Open browser with unique session ID const url = `http://127.0.0.1:${availablePort}?id=${this.sessionId}`; open(url).catch((error) => { console.warn("Failed to open browser:", error); resolve({ data: { browserOpenFailed: true } }); this.shutdown(); }); }); } catch (error) { await this.shutdown(); throw error; } } } export class CallbackServer { private server: Server | null = null; private port: number; private sessionId = Math.random().toString(36).substring(7); // ... other properties async promptUser( config: CallbackServerConfig = {} ): Promise<CallbackResponse> { const { initialData = null, timeout = 300000 } = config; this.config = config; try { const availablePort = await this.findAvailablePort(); this.server = createServer(this.handleRequest); this.server.listen(availablePort, "127.0.0.1"); // Set up promise to handle user selection return new Promise<CallbackResponse>((resolve, reject) => { this.promiseResolve = resolve; this.promiseReject = reject; // ... server setup code // Open browser with unique session ID const url = `http://127.0.0.1:${availablePort}?id=${this.sessionId}`; open(url).catch((error) => { console.warn("Failed to open browser:", error); resolve({ data: { browserOpenFailed: true } }); this.shutdown(); }); }); } catch (error) { await this.shutdown(); throw error; } } } This server: Dynamically finds an available port Creates a unique session ID for each request Serves the UI bundle Opens the browser to display options Receives the user's selection through a callback Resolves the promise with the selected data Dynamically finds an available port Creates a unique session ID for each request Serves the UI bundle Opens the browser to display options Receives the user's selection through a callback Resolves the promise with the selected data Integration with MCP tool Integration with MCP tool We've applied this approach to enhance our 21st_magic_component_builder tool, which generates UI components: export class CreateUiTool extends BaseTool { name = UI_TOOL_NAME; description = UI_TOOL_DESCRIPTION; // ... schema definition async execute({ message, searchQuery, absolutePathToCurrentFile, context, }: z.infer<typeof this.schema>): Promise<{ content: Array<{ type: "text"; text: string }>; }> { try { // Fetch UI component variations from API const response = await twentyFirstClient.post<{ data1: { text: string }; data2: { text: string }; data3: { text: string }; }>("/api/create-ui-variation", { message, searchQuery, fileContent: await getContentOfFile(absolutePathToCurrentFile), context, }); // Handle billing or error cases if (response.status !== 200) { open("https://21st.dev/settings/billing"); return { content: [ { type: "text" as const, text: response.data.text as string, }, ], }; } // Create server and prompt user through browser const server = new CallbackServer(); const { data } = await server.promptUser({ initialData: { data1: response.data.data1, data2: response.data.data2, data3: response.data.data3, }, }); // Process user selection and return formatted response const componentData = data || { text: "No component data received. Please try again.", }; // Return formatted response to user // ... } catch (error) { console.error("Error executing tool", error); throw error; } } } export class CreateUiTool extends BaseTool { name = UI_TOOL_NAME; description = UI_TOOL_DESCRIPTION; // ... schema definition async execute({ message, searchQuery, absolutePathToCurrentFile, context, }: z.infer<typeof this.schema>): Promise<{ content: Array<{ type: "text"; text: string }>; }> { try { // Fetch UI component variations from API const response = await twentyFirstClient.post<{ data1: { text: string }; data2: { text: string }; data3: { text: string }; }>("/api/create-ui-variation", { message, searchQuery, fileContent: await getContentOfFile(absolutePathToCurrentFile), context, }); // Handle billing or error cases if (response.status !== 200) { open("https://21st.dev/settings/billing"); return { content: [ { type: "text" as const, text: response.data.text as string, }, ], }; } // Create server and prompt user through browser const server = new CallbackServer(); const { data } = await server.promptUser({ initialData: { data1: response.data.data1, data2: response.data.data2, data3: response.data.data3, }, }); // Process user selection and return formatted response const componentData = data || { text: "No component data received. Please try again.", }; // Return formatted response to user // ... } catch (error) { console.error("Error executing tool", error); throw error; } } } User Experience Flow Here's how the user experience flows when requesting a UI component: Tool Invocation: The AI assistant invokes the 21st_magic_component_builder tool when a user requests a new UI component. API Request: The tool sends a request to the 21st.dev API to generate multiple UI component variations based on the user's message and context. Browser Launch: A local server starts and a browser window automatically opens, displaying the generated UI component options. User Interaction: The user can view, interact with, and select their preferred component variation. Selection Capture: When the user makes a selection, the browser sends the selection back to the callback server. Execution Resumption: The server shuts down, and execution resumes with the selected component data. Integration Guidance: The AI assistant receives the selected component and provides guidance on integrating it into the user's codebase. Tool Invocation: The AI assistant invokes the 21st_magic_component_builder tool when a user requests a new UI component. Tool Invocation API Request: The tool sends a request to the 21st.dev API to generate multiple UI component variations based on the user's message and context. API Request Browser Launch: A local server starts and a browser window automatically opens, displaying the generated UI component options. Browser Launch User Interaction: The user can view, interact with, and select their preferred component variation. User Interaction Selection Capture: When the user makes a selection, the browser sends the selection back to the callback server. Selection Capture Execution Resumption: The server shuts down, and execution resumes with the selected component data. Execution Resumption Integration Guidance: The AI assistant receives the selected component and provides guidance on integrating it into the user's codebase. Integration Guidance This approach creates a seamless experience that allows users to make informed decisions about UI components while maintaining the overall MCP workflow. Security and Privacy Considerations Our implementation takes several security measures: Local Hosting: All communication happens locally on the user's machine (127.0.0.1) Unique Session IDs: Each browser session has a unique ID to prevent cross-session interference Timeout Mechanism: Sessions automatically time out after a configurable period (default 5 minutes) Port Safety: The server dynamically finds an available port to avoid conflicts Local Hosting: All communication happens locally on the user's machine (127.0.0.1) Local Hosting Unique Session IDs: Each browser session has a unique ID to prevent cross-session interference Unique Session IDs Timeout Mechanism: Sessions automatically time out after a configurable period (default 5 minutes) Timeout Mechanism Port Safety: The server dynamically finds an available port to avoid conflicts Port Safety Conclusion The browser-based approach to MCP communication represents a significant improvement to the user experience when working with tools that benefit from visual interaction. By bridging the gap between the powerful capabilities of MCP and the interactive nature of web interfaces, we've created a more intuitive workflow for users. This approach is particularly valuable for UI component generation, where visual representation is crucial for making informed decisions. However, the pattern could be extended to other tools that would benefit from user interaction during execution. Source code is available on GitHub. GitHub