Skip to main content
Version: Latest

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 MCPWith MCP
Model emits 50KB JSON, hopes it parsesModel calls add_row(cells: [50,50]) — server validates + executes
You build prompts that beg for the right shapeTool schema forces the shape
1 mistake → entire output brokenEach mutation atomic, reversible
Slow turnaround on errorsErrors 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:

CredentialWhere you configure itWho sends it to DragbleDomain 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 varsYour backend AI orchestratorNone — backend server credential
mcp_client_key (db_mcp_client_a1b2c3d4e5f6g7h8)In local AI-client config (Claude Code, OpenCode, Cursor)Third-party MCP clientsNone — 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:

  1. Plan allows MCP (core.mcp feature enabled)
  2. 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 / reloadSame id → same MCP session resumes
Cross-deviceSame id → both devices share the session
Server restartSessions restored automatically by Dragble
Tab closedSession 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-1Alice-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 firstSession locked toWhat's blocked
Your backend calls a toolbackendPairing codes rejected with SESSION_CONTROLLED_BY_BACKEND
End-user pairs via codepaired_clientBackend 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.

ToolFreeStarterProBusinessEnterprise
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:

EndpointDefault limitScope
Session establishmentStarter: 100/minute
Pro: 250/minute
Business: 500/minute
Enterprise: unlimited
per MCP key
Tool calls300/minuteper 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

  1. Frontend creates the session via the SDK. sdk.joinMCP({ id }) prepares the editor for live AI updates. You provide a deterministic id derived from your domain entities — e.g. user-42-doc-99.

  2. Frontend ships the sessionId to your backend over your own auth-protected channel (REST, GraphQL, realtime transport — whatever your app uses internally).

  3. 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-provided sessionId in each action. It does not use pairing codes.

  4. Your backend runs the AI loop. For every design action the model proposes, your backend injects sessionId server-side and sends the action to MCP.

  5. 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_key and sessionId.
  • The mcp_key never leaves your backend. Treat it like a Stripe secret.
  • sessionId links the editor and backend flow. One ID identifies the live design session.
  • args.sessionId = sessionId is mandatory. Strip sessionId from 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 })): use editor_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