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.
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
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
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;
}
}
}
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
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;
}
}
}
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.
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
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.