MCP — Model Context Protocol
The Dragble MCP server lets AI agents control a live Dragble editor through the Model Context Protocol. AI calls structured tools, MCP routes those calls to the paired editor, and the editor renders changes live on canvas. No prompt-engineering, no JSON hallucination, no broken output.
Why MCP?
Traditional AI integrations make the model emit a JSON design and you parse it. This breaks constantly — invalid JSON, hallucinated fields, missing IDs. MCP fixes this by giving the model structured tools with strict schemas. Every call produces guaranteed-valid editor state. Zero hallucination on structure.
| Without MCP | With MCP |
|---|---|
| Model emits 50KB JSON, hopes it parses | Model calls add_row(cells: [50,50]) — server validates + executes |
| You build prompts that beg for the right shape | Tool schema forces the shape |
| 1 mistake → entire output broken | Each mutation atomic, reversible |
| Slow turnaround on errors | Errors caught at tool boundary, model corrects |
What you can build with it
- AI design copilot in your SaaS: "Make the hero darker and add a CTA" → AI calls
update_row+add_button - Backend-managed design copilot: your backend owns the LLM loop while the user's live editor receives each tool call
- Terminal-driven design: pair OpenCode/Claude Code to a live editor session and watch AI build the design as you describe it
- Multi-modal editing: voice → AI → MCP tools → live editor
Three credentials
Dragble uses a three-credential model to separate browser-safe editor traffic, backend orchestration, and third-party AI-client pairing:
| Credential | Where you configure it | Who sends it to Dragble | Domain check |
|---|---|---|---|
editor_key (db_pxl81cxn92wignwx) | Once, at editor mount: <DragbleEditor editorKey="db_..." /> | The editor iframe (automatic, behind the scenes) | Required — Origin must match your allowed domains list |
mcp_key (db_mcp_a1b2c3d4e5f6g7h8) | In your backend env vars | Your backend AI orchestrator | None — backend server credential |
mcp_client_key (db_mcp_client_a1b2c3d4e5f6g7h8) | In local AI-client config (Claude Code, OpenCode, Cursor) | Third-party MCP clients | None — pairing-scoped client credential |
When you call sdk.joinMCP({ id }) from the frontend, you only pass the session id — the editor already has the editor_key from initialization and uses it to make the underlying request. The customer never has to handle editor_key again after mount.
Critical: Never put mcp_key or mcp_client_key in browser code. The backend key can manage sessions, and the client key can pair third-party tools to live editor sessions. Treat both like secrets.
Generate both MCP keys from your Dragble dashboard: Projects → {your project} → MCP Key. Use Backend MCP Key for server orchestration and Client MCP Pairing Key for Claude/OpenCode/Cursor/Codex-style configs.
Enabling MCP in the SDK
MCP is on by default when your plan includes it. You can still set the flag explicitly, or set it to false to turn MCP off for an embed:
// React
<DragbleEditor editorKey="db_..." />
// Vanilla JS
sdk.init({ id: "editor-container" });
// Optional opt-out for embeds that should not expose MCP controls
sdk.init({ id: "editor-container", features: { mcp: false } });
MCP also requires a Starter plan or higher. Both conditions must be true:
- Plan allows MCP (
core.mcpfeature enabled) - SDK has not opted out (
features.mcp !== false)
If the plan doesn't include MCP, joinMCP() and startMCPPairing() return error MCP_NOT_AVAILABLE_ON_PLAN.
If features.mcp is set to false, MCP session calls return error MCP_DISABLED_BY_SDK.
Bring Your Own ID (BYOI)
When you start an MCP session, you provide the session id. Same id always returns the same session — even after browser refresh, server restart, or device switch.
const id = `user-${userId}-doc-${documentId}`;
await sdk.joinMCP({ id });
| Refresh / reload | Same id → same MCP session resumes |
|---|---|
| Cross-device | Same id → both devices share the session |
| Server restart | Sessions restored automatically by Dragble |
| Tab closed | Session persists — reconnect later with the same id |
ID format and validation
The id is validated at four layers (SDK, editor, MCP server, database). It must match:
/^[a-zA-Z0-9_-]{8,128}$/
Rules:
- 8–128 characters — short IDs are guessable, long IDs waste storage
- Only
a-z A-Z 0-9 - _— the id is used as an identifier and travels through URLs; special characters break routing - Case-sensitive:
alice-doc-1≠Alice-doc-1
The id should be deterministic, not random. It maps to your domain entities (user + document, workspace + template, org + campaign). Same user editing the same document should always produce the same id. This is what makes sessions survive browser refreshes and server restarts.
// Recommended: derive from your domain entities
sdk.joinMCP({ id: `user-${userId}-doc-${docId}` });
sdk.joinMCP({ id: `workspace_abc_template_xyz` });
sdk.joinMCP({ id: `org-123-campaign-456` });
// Valid but NOT recommended: random UUIDs lose session continuity
// Every page refresh creates a new session and loses AI context
sdk.joinMCP({ id: crypto.randomUUID() });
ID generation is your responsibility. Choose a scheme that's:
- Stable across refreshes (same id = same session)
- Unique per logical session (no accidental collisions across users)
- Namespaced to your project (e.g., prefix with your customer id)
One controller per session
Each session can be controlled by one AI source at a time — either your backend or an end-user's AI client (OpenCode, Claude Code), never both simultaneously.
| Who connects first | Session locked to | What's blocked |
|---|---|---|
| Your backend calls a tool | backend | Pairing codes rejected with SESSION_CONTROLLED_BY_BACKEND |
| End-user pairs via code | paired_client | Backend tool calls rejected |
This prevents two AI controllers from conflicting on the same design. The lock is set on first interaction and lasts for the session's lifetime.
Plan-based tool access
Each Dragble plan grants access to a different set of content tools. When an AI agent connects through MCP, it can only call the tools your plan allows — restrictions are enforced at the tool-call boundary.
| Tool | Free | Starter | Pro | Business | Enterprise |
|---|---|---|---|---|---|
| Paragraph, Heading, Button, Image, Divider, Menu, HTML, Social, Video, Spacer | ✅ | ✅ | ✅ | ✅ | ✅ |
| Table | ❌ | ✅ | ✅ | ✅ | ✅ |
| Timer | ❌ | ✅ | ✅ | ✅ | ✅ |
| Form | ❌ | ✅ | ✅ | ✅ | ✅ |
| Carousel | ❌ | ✅ | ✅ | ✅ | ✅ |
If the AI tries to call a tool not allowed on your plan, the response is a clear error message — for example:
Tool 'table' is not available on your current plan. Upgrade to access this tool.
Most AI clients (Claude Code, OpenCode, Cursor) handle this gracefully and adapt their plan-of-action. End-users see no crashes, only the design tools you've paid for.
Enterprise plans can be configured per-customer through the Dragble dashboard — talk to sales for plans with bespoke tool sets.
Force-destroy a session from your backend
If you need to terminate a user's MCP session immediately — for example, when their subscription ends, their account is suspended, or they're banned — your backend can force-destroy the session over HTTP:
curl -X DELETE https://mcp.dragble.com/sessions/$SESSION_ID \
-H "X-API-Key: $MCP_KEY"
The session is removed from Dragble's servers, any active AI client connection is closed, and pairing codes for that session are revoked. The user cannot resume the session even if they still hold a pairing code or the SDK still has the id cached.
Rate limits
MCP traffic is rate-limited per credential to protect against runaway agents and accidental abuse:
| Endpoint | Default limit | Scope |
|---|---|---|
| Session establishment | Starter: 100/minute Pro: 250/minute Business: 500/minute Enterprise: unlimited | per MCP key |
| Tool calls | 300/minute | per session |
Session establishment limits apply to new MCP sessions per minute. Reconnecting or rejoining an existing non-expired session with the same id does not count against this limit.
Rate-limited responses return HTTP 429 with a Retry-After header. Session establishment throttles use the MCP_SESSION_RATE_LIMITED error code. Well-behaved AI clients back off automatically. If you hit these limits in normal operation, contact us — limits are tunable per account.
Two integration architectures
The MCP server supports two live-editor flow patterns. Pick the one that fits your product. Both use your app's deterministic id through the SDK so the editor is the design-state owner.
Architecture A — Editor-driven (end-user pairs their AI client)
User clicks "AI assist" in your app → editor creates a session → you explicitly generate a pairing code → user enters code in OpenCode / Claude Code / Codex → AI mutations stream live to the editor canvas. Pairing codes are only used in this architecture.
// Your frontend
await sdk.joinMCP({ id: `user-${userId}-doc-${docId}` });
// Explicitly generate a pairing code (not auto-generated)
const { code } = await sdk.getPairingCode();
showPairingCode(code); // "47289153" — stable for the active MCP session
// User types this code into their AI agent chat
MCP config only makes Dragble tools available to the AI client. To control the live editor, the user still needs to paste or type the pairing-code prompt into their AI agent chat:
Connect with Dragble MCP using pairing code 47289153
The AI client should infer the pair_with_editor tool call from that sentence and pass 47289153 as the code. If the client supports manual tool entry, the user can run pair_with_editor 47289153 directly.
Architecture B — Your own AI backend (paired with live editor)
This is the production-grade pattern when you want to ship your own AI design copilot. Your backend owns the LLM key, calls Dragble through MCP, and the editor renders updates live on the user's canvas. No pairing code is needed — your backend uses your project's backend MCP key (mcp_key).
┌─────────────────────┐ ┌─────────────────────┐
│ YOUR FRONTEND │ │ YOUR AI BACKEND │
│ (browser) │ │ (server) │
│ │ │ │
│ • Dragble Editor │ │ • Your AI logic │
│ (via SDK) │ │ • Your model key │
│ • Your chat UI │ │ • MCP connection │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
│ session id │ session id
│ from SDK │ for AI edits
▼ ▼
┌─────────────────────────────────────────────────────┐
│ DRAGBLE MCP │
│ │
│ Validates each request and routes tool calls │
│ to the live editor over WebSocket. │
└─────────────────────────────────────────────────────┘
How the pieces talk
-
Frontend creates the session via the SDK.
sdk.joinMCP({ id })prepares the editor for live AI updates. You provide a deterministicidderived from your domain entities — e.g.user-42-doc-99. -
Frontend ships the
sessionIdto your backend over your own auth-protected channel (REST, GraphQL, realtime transport — whatever your app uses internally). -
Your backend connects with the backend MCP key and uses the same
sessionId. It sends AI design actions to Dragble's MCP endpoint and includes the frontend-providedsessionIdin each action. It does not use pairing codes. -
Your backend runs the AI loop. For every design action the model proposes, your backend injects
sessionIdserver-side and sends the action to MCP. -
Each design update appears in the editor while the user stays on the canvas.
Frontend
// React example (any framework works)
<DragbleEditor
editorKey="db_pxl81cxn92wignwx"
onReady={async (sdk) => {
const id = `user-${userId}-doc-${docId}`;
const { sessionId } = await sdk.joinMCP({ id });
// Ship sessionId to YOUR backend — your auth, your transport.
await fetch("/api/ai/init", {
method: "POST",
body: JSON.stringify({ sessionId }),
});
// Optional: subscribe to live AI activity for UI overlays.
sdk.onAIToolFired((evt) => {
console.log("AI fired", evt.kind, evt.args);
});
}}
/>
Backend
// app/api/ai/chat.ts — runs SERVER-SIDE only
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import OpenAI from "openai";
const transport = new StreamableHTTPClientTransport(new URL("https://mcp.dragble.com/mcp"), {
requestInit: {
headers: { "X-API-Key": process.env.DRAGBLE_MCP_KEY! },
},
});
const mcp = new Client({ name: "my-ai-backend", version: "1.0.0" }, { capabilities: {} });
await mcp.connect(transport);
// Convert MCP tools to OpenAI function-calling format
const { tools } = await mcp.listTools();
const functions = tools.map((t) => ({
type: "function" as const,
function: {
name: t.name,
description: t.description ?? "",
parameters: t.inputSchema ?? { type: "object", properties: {} },
},
}));
export async function handleChat(sessionId: string, userMessage: string) {
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const messages: any[] = [
{ role: "system", content: "You design emails by calling MCP tools." },
{ role: "user", content: userMessage },
];
for (let round = 0; round < 25; round++) {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools: functions,
});
const choice = response.choices[0];
if (!choice.message.tool_calls?.length) break;
messages.push(choice.message);
for (const tc of choice.message.tool_calls) {
const args = JSON.parse(tc.function.arguments);
args.sessionId = sessionId; // ALWAYS inject server-side — never trust the LLM
const result = await mcp.callTool({ name: tc.function.name, arguments: args });
messages.push({
role: "tool",
tool_call_id: tc.id,
content: result.content[0].type === "text" ? result.content[0].text : "{}",
});
}
}
}
Key guarantees
- The LLM key never leaves your backend. Browser only sees
editor_keyandsessionId. - The
mcp_keynever leaves your backend. Treat it like a Stripe secret. sessionIdlinks the editor and backend flow. One ID identifies the live design session.args.sessionId = sessionIdis mandatory. StripsessionIdfrom the schema you hand to the LLM (or just overwrite after parsing); a wrong session id sent by the model breaks isolation.- Once your backend calls any mutating tool, the session locks to
controlledBy: "backend"— end-users cannot pair their own AI client to it. This prevents two AI sources from racing on the same design.
Disconnecting and session cleanup
disconnectMCP() permanently destroys the session — Dragble removes all session data from its servers and the session cannot be reopened:
const { destroyed } = await sdk.disconnectMCP();
Your backend can also force-destroy a session server-side (e.g., when a user's subscription ends or their account is blocked):
curl -X DELETE https://mcp.dragble.com/sessions/user-42-doc-99 \
-H "X-API-Key: db_mcp_your_key_here"
If a session is not explicitly disconnected, Dragble cleans it up automatically after inactivity.
Security summary
- Browser flows (loading the editor,
sdk.joinMCP({ id })): useeditor_key— domain-checked, safe to expose. - Backend AI flows (your AI orchestrator): use
mcp_key— server-only, never expose in browser. - Third-party AI clients (OpenCode, Claude Code, Cursor, Codex): use
mcp_client_key— client-config only, pairing-scoped, never expose in browser. - The credentials don't overlap: AI tooling rejects
editor_key, client keys cannot manage raw backend sessions, and browser flows ignore MCP keys. Cross-credential misuse is blocked by design. - One controller per session: backend OR paired client, never both. Prevents two AI sources from conflicting on the same design.
What's next
- Get started fast → Quick Start
- Add MCP keys → Credentials & Security
- Connect OpenCode / Claude Code → AI Client Setup
- Browse the AI agent skills → Agent Skills
- See template examples → Template Library