I’ve been exploring how to build an MCP server with Bubble and decided to share my approach and implementation. You can test it here:
https://username:password@mcp-demo.bubbleapps.io/
Editor: https://bubble.io/page?id=mcp-demo
It uses API workflows I built to act as MCP tools. This setup allows users to authenticate and interact with the app through clients like Claude Desktop, Cursor, and others. Users can create, delete, and list tasks using the MCP protocol.
I’ve also created a test user and added this user token in the MCP config JSONs (so that anyone can test easily without creating a new user) for both Claude and Cursor.
What Is MCP?
If you’re new to MCP, it’s a protocol that allows an AI model (like Claude) to discover and use tools dynamically. It can ask the server what tools are available, understand how to use them, and call them on demand.
A typical flow looks like this:
-
initialize: The model connects and gets metadata from the server, such as its name, version, and capabilities.
-
tools/list: The model requests the available tools, and the server returns a list with their names, parameters, and output schemas.
-
tools/call: The model calls one of the tools with specific arguments. The server then executes the tool and returns the results in a structured JSON-RPC format.
Connecting to Bubble via mcp-remote
MCP clients communicate via stdio (standard input/output), but Bubble only provides HTTP APIs. To bridge this gap, I use mcp-remote - an official npm library by Anthropic that translates between the two protocols.
Users don’t need to install anything. The MCP client configuration looks like this:
{
"command": "npx",
"args": ["-y", "mcp-remote", "YOUR_BUBBLE_API_URL"]
}
The client automatically runs mcp-remote via npx, which handles all protocol translation. This means your Bubble HTTP endpoint can communicate with MCP clients seamlessly.
Main Challenges
Successfully running an MCP server in Bubble comes down to solving two main problems: handling dynamic incoming data and crafting perfectly structured JSON-RPC 2.0 responses.
Challenge 1: Handling Dynamic Inputs
A single API workflow in Bubble (e.g. /mcp) will handle every request to your MCP server. The issue is that Bubble’s “Detect Data” can only define an API’s data structure once. This is a problem because each MCP method (initialize, tools/list, and tools/call) sends a different JSON payload from the MCP client. If you detect data for one method, Bubble won’t recognize some of the parameters sent by the others.
Solution: The “Master API Request Data”
Natively, there is no easy solution in Bubble, so we need to either try to parse the raw request body or create a universal schema. I chose the latter and created a “Master API Request Data” - a universal schema that includes every possible parameter from all MCP methods.
This is not an easy task, as the arguments sent depend on the tools available in your MCP server and their schemas. Constructing this JSON is prone to errors… To mitigate this, I built a tool that helped me generate the JSON. I’m not sure how well it will perform for other MCP servers, but I am open to sharing the link so anyone can use it. Even if it fails, it can serve as a starting point and be manually adjusted for a particular use case.
Challenge 2: Building Correct Responses
Returning the right response format is just as critical as handling the request. MCP clients are strict about the JSON-RPC 2.0 protocol and expect a specific structure with fields like jsonrpc, id, and either a result or an error object. If the response is malformed, the MCP connection will fail. If you build your Bubble server to correctly handle the request and provide the right response for each of these three methods—initialize, tools/list, and tools/call—your MCP server will work nicely.
I have prepared a list of example responses for each method:
Important Rule: Every response you send must include the exact same id that you received in the request.
The initialize Response (The Handshake)
This is an example response to the very first call from MCP client with initialize method. It confirms your server is online and ready to communicate.
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "YourBubbleAppName",
"version": "1.0.0"
}
}
}
The tools/list Example Response (The Tool Menu)
After the ‘handshake’, the client will request tools/list. This response provides a “menu” of all available tools. The AI uses each tool’s description to decide which one to use and the input schema to understand how to use it.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "create_new_task",
"description": "Creates a new task with a title.",
"inputSchema": {
"type": "object",
"properties": {
"title": {
"type": "string"
}
},
"required": [
"title"
]
}
}
]
}
}
The tools/call Success Response (Execution Result)
When the client calls a tool, you must respond after your workflow executes. If it runs successfully, return a result object.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"status": "success",
"message": "The task was created successfully.",
"taskId": "12345x67890"
}
}
The tools/call Error Response
If your workflow fails, return an error object instead of a result object. This tells the client the operation failed and allows it to handle the error gracefully.
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params",
"data": "A more detailed error message can go here."
}
}
Using These Templates in Main /mcp Endpoint
A single API workflow in Bubble will handle every request to your MCP server. The workflow steps should check the method field of the request data and take actions accordingly using conditional actions.
For initialize:
-
Add a “Return data from API” action
-
Set the condition: Only when Request Data’s method is “initialize”
-
Use the MCP initialize Response JSON example (see above)
-
Crucially, make the id field dynamic by using the id from the request data
For tools/list:
-
Add another “Return data from API” action
-
Set the condition: Only when Request Data’s method is “tools/list”
-
Use the MCP tools/list Response JSON (see example above)
-
Make the id field dynamic by using the id from the request data
For tools/call:
- For each MCP tool add a step with a condition like Only when Request Data’s method is “tools/call” AND Request Data’s params name is “your_tool_name”
This step can execute your logic, call an API workflow built for this specific tool (which I did in this demo), or trigger a custom event.
Always Reply to the Client
After a tool is executed, you must send a response back using a “Return data from API” action. The AI client pauses and waits for this response before proceeding. If no response is sent, the client will time out, and the operation will fail. The key is maintaining the exact JSON-RPC 2.0 structure while making the values dynamic.
Building MCP Tools in Bubble
Each MCP tool can be created in two ways:
-
Public API Workflows (with auth)
-
Custom Events
-
Simpler and safer - not publicly exposed
-
Return values directly without the API connector
-
I used Public API workflows because I relied on Swagger and a custom tool I built to create valid JSON templates for the initialize and tools/list methods. However, Custom Events are likely a better choice for production since they’re not publicly exposed and can return values directly to the /mcp. In my case, I had to use the API Connector to recieve and send data (e.g., “new task created with ID: XXXXX”) back to the main /mcp workflow so it could be returned to the client.
Security and Access Control
My setup leverages Bubble’s authentication and privacy rules, ensuring clients can only access data they’re permitted to. It’s also easy to implement role-based access so different users can see or use different tools—for example, admins can have access to more tools than regular users.
I hope this post was helpful, and I’d love to hear any feedback or suggestions for improvement. I’m also curious to learn how others have tackled the challenges I described above. I’m not claiming this is the only or best way to build MCPs—just that it can be done with some effort. If there’s enough interest, I can share the helper tool I used to automatically generate JSON
Cheers,
Branimir