Why Cursor is a different case
Lovable, v0, and Bolt are hosted environments — they run your app and, increasingly, hand you a database alongside it. Cursor is not that. It is an AI code editor on your own machine, working on your own files. There is no “Cursor database” to toggle on. That sounds like more work, and for raw infrastructure it would be — but Cursor has one capability the hosted builders cannot match for this job.
Cursor speaks the Model Context Protocol natively. You can connect MCP servers in .cursor/mcp.json and the agent reads and writes through them directly. For a database, that is the difference between Cursor guessing at your tables and Cursor knowing them.
The MCP-first approach
There is a well-worn pattern of pointing Cursor at a raw Postgres MCP server so the agent can inspect a schema and run queries. It works — for a database you are already administering. The catch is that it assumes you are administering one: designing tables, writing migrations, granting access.
Cradler's MCP server keeps the same “agent sees the real schema” benefit but removes the administration underneath it. The schema auto-evolves, so there is nothing to migrate. Access is a set of dashboard toggles, not SQL grants. And the same project holds file storage. Cursor still works against ground truth — it just is not ground truth you have to maintain by hand.
Connecting Cradler to a Cursor project, step by step
The MCP server is what makes Cursor accurate; do that step first.
- 01
Create a Cradler project
Sign up at cradler.ai and create a project. You get an isolated PostgreSQL database, file storage, a gateway URL, a project ID, an anon key, and a service key.
- 02
Add the Cradler MCP server to Cursor
Add @cradler/mcp to your project's .cursor/mcp.json with your gateway URL, project ID, and key. Cursor's agent can then read your real schema and your data directly, instead of guessing.
- 03
Install @cradler/sdk in your project
Run your package manager to add @cradler/sdk, and put the gateway URL, project ID, and key in environment variables. The SDK is the typed client your app code will use at runtime.
- 04
Prompt Cursor against the live schema
Ask Cursor in the Composer to read and write your data. With MCP connected it inspects the actual tables, so the @cradler/sdk code it writes matches your data — and the schema grows as you add fields.
The MCP config
Project-specific tools belong in a project-scoped config. Create .cursor/mcp.json at the root of your repo — Cursor reads it automatically:
{
"mcpServers": {
"cradler": {
"command": "npx",
"args": ["-y", "@cradler/mcp"],
"env": {
"CRADLER_URL": "https://gateway.cradler.ai",
"CRADLER_PROJECT_ID": "<your-project-id>",
"CRADLER_KEY": "<your-service-key>"
}
}
}
}Reload Cursor and the cradlerserver appears in Settings. From then on, ask the agent “what columns does the orders table have?” and it answers from the live schema. Keep this file in .gitignore, or use a placeholder and an environment variable — the service key bypasses table permissions and must not be committed.
The runtime code
MCP is how Cursor understands your data while it codes. @cradler/sdk is what your app runs. Say you are building an orders feature; the data layer Cursor writes looks like this:
import { createClient } from "@cradler/sdk";
const cradler = createClient({
url: process.env.CRADLER_URL!,
projectId: process.env.CRADLER_PROJECT_ID!,
apiKey: process.env.CRADLER_KEY!,
});
type Order = {
id: string;
customerEmail: string;
total: number;
status: "pending" | "paid" | "shipped";
createdAt: string;
};
// A typed collection — Cursor knows every field.
const orders = () => cradler.from<Order>("orders");
export async function placeOrder(email: string, total: number) {
const { rows } = await orders().insert({
customerEmail: email,
total,
status: "pending",
});
return rows[0];
}
export async function markPaid(id: string) {
await orders().update({ status: "paid" }).eq("id", id);
}
// Paid orders over $100, newest first — first page of 20.
export async function highValueOrders(page = 0) {
const { rows, count } = await orders()
.select("id", "customerEmail", "total", "createdAt")
.eq("status", "paid")
.gt("total", 100)
.order("createdAt", { desc: true })
.limit(20)
.offset(page * 20);
return { orders: rows, total: count };
}
// Look up one order, or null if it does not exist.
export async function findOrder(id: string) {
return orders().select().eq("id", id).first();
}Passing a row type to from<Order>() gives Cursor end-to-end typing — every filter field and every result property is checked. Combined with what MCP tells it about the real schema, Cursor is no longer guessing on either side.
File uploads
An invoice PDF for each order? Storage is part of the same Cradler project:
async function attachInvoice(orderId: string, pdf: Blob) {
const path = `invoices/${orderId}.pdf`;
await cradler.storage.upload(path, pdf, {
contentType: "application/pdf",
});
await cradler
.from("orders")
.update({ invoicePath: path })
.eq("id", orderId);
// A temporary signed URL to hand back to the customer.
return cradler.storage.getUrl(path);
}Gotchas worth knowing
Never commit the service key. The MCP config and your .env both hold it. It bypasses every table permission, so it belongs in .gitignore or a secrets manager — not in your repo.
Trust MCP over the model's memory. If Cursor writes a query against a column that does not exist, the fix is usually that the MCP server was not connected for that turn. With it connected, the agent checks the schema instead of recalling it. Confirm cradler is listed and enabled in Settings.
Let inserts shape the schema. You do not write a CREATE TABLE. The first orders().insert(...) creates the table; a later insert with a new field adds the column. Renames and drops are deliberate, confirmed, backed-up dashboard actions — never something an agent does on its own.
Why Cradler suits Cursor
Cursor is at its best when it has real context. Cradler is built to give it: an MCP server for the live schema, a typed @cradler/sdk for the runtime, per-project generated types, and an llms.txt the agent can read. Underneath it is standard PostgreSQL, so nothing about the data is locked to Cradler — but you never have to administer it.