Agent writes & ingest

Pro+ endpoints can opt in to letting agents add, edit, and delete rows — and add brand-new sources — through MCP tools. Free endpoints stay read-only by design.

The mental model

Every endpoint has an allow_writes flag, off by default. When you flip it on (Pro+ only), schema-gen produces three new tools per writeable table alongside the read tools:

  • insert_<table> — add a row
  • update_<table>_by_id — edit a row by primary key
  • delete_<table>_by_id — remove a row by primary key

Plus four built-in ingest tools for adding new sources without leaving the chat:

  • ingest_url — fetch a URL (markdown, crawl, or structured JSON)
  • ingest_file — small files inline as base64
  • request_upload_url + ingest_uploaded_file — large files via signed PUT

All seven tools require a token with the mcp:write OAuth scope. The agent can't elevate scope on its own — only the human user can grant it via the consent screen.

Enable writes on an endpoint

  1. Visit /endpoints and pick the endpoint.
  2. Find the Agent writes & ingest card.
  3. Toggle Allow writes on. Disabled with an upgrade hint if you're on the free tier.

Toggling fires a background rebuild-tools job so the endpoint's tool manifest immediately gains (or loses) the write tools. Existing connected agents need to fetch tools/list again to see them — most clients refresh on their own.

Granting an agent write access

Connecting an agent (Claude.ai, Cursor, etc.) goes through OAuth 2.0. On the consent screen at shipmcp.io, you'll see a separate "Also grant write access" checkbox whenever your account has at least one endpoint with writes enabled. Tick it before clicking Allow — the issued token will carry scope: "mcp:write".

The checkbox shows a warning block: "This app will be able to add, edit, and delete rows in your endpoints." Treat it like any high-trust grant — only tick for apps you trust.

info

Already connected without write scope?

Disconnect the connector in your client and reconnect. The consent screen will show the write checkbox and the new token will replace the read-only one.

How writes execute

Writes are asynchronous. The agent calls a write tool → ShipMCP enqueues a job → returns {"status":"queued","job_id":"..."} instantly → a worker drains the queue and applies the change to your Neon project.

Why async: workers ship audit logging, retry semantics, and idempotency that the agent's request handler shouldn't have to own. The agent gets back a handle it can poll.

// Agent calls insert_documents
{
  "method": "tools/call",
  "params": {
    "name": "insert_documents",
    "arguments": { "title": "Note", "content": "...", "url": "note://1", "kind": "text" }
  }
}

// Server returns immediately
{
  "result": {
    "content": [{ "type": "text", "text": "{ \"status\": \"queued\", \"job_id\": \"abc-123\" }" }]
  }
}

// Agent polls for the result
{
  "method": "tools/call",
  "params": { "name": "get_write_job_by_id", "arguments": { "job_id": "abc-123" } }
}

// After ~1-3 seconds: status="succeeded", result={"id": 42}

Ingest tools

ingest_url

Pass a URL and a format. We use Cloudflare Browser Rendering with waitUntil: "load" + 3-second post-load wait so SPAs (Notion sites, React apps) hydrate before extraction.

// Three modes
{ "url": "https://example.com/article", "format": "markdown" }
{ "url": "https://docs.example.com",   "format": "crawl", "max_depth": 2, "max_pages": 50 }
{ "url": "https://example.com/list",   "format": "json",  "table_name": "products" }

ingest_file

Inline base64 path for files up to ~7 MB (Worker memory limit on decode). Use for short PDFs, transcripts, small images. Anything larger: request_upload_url.

{
  "filename": "notes.pdf",
  "content_base64": "JVBERi0xLjQKJ...",
  "mime_type": "application/pdf"  // optional, inferred from extension
}

request_upload_url + ingest_uploaded_file

Two-step large-file path. Step 1 mints a 5-minute signed PUT URL; step 2 kicks ingestion against the uploaded R2 key. Handles arbitrary file sizes up to your plan's per-file cap (Pro: 100 MB, Scale: 500 MB).

// 1. Request the URL
request_upload_url({ "filename": "podcast.mp3" })
// → { "upload_url": "https://mcp.shipmcp.io/.../upload?t=...", "r2_key": "uploads/.../podcast.mp3", "expires_in": 300 }

// 2. PUT bytes to upload_url (any HTTP client, no auth header needed)
PUT /your-slug/upload?t=eyJ...
Content-Type: audio/mpeg
<binary body>

// 3. Kick ingestion
ingest_uploaded_file({ "r2_key": "uploads/.../podcast.mp3" })

Failure handling

Agent-initiated writes/ingests that fail do not mark your endpoint status='error'. A bad URL or malformed file from an agent shouldn't lock you out of an otherwise-healthy endpoint. Failures surface only via get_write_job_by_id:

{
  "job_id": "abc-123",
  "tool": "ingest_url",
  "status": "failed",
  "result": { "error": "Got near-empty content from https://... — page may require auth..." }
}

What's logged

Every successful write or ingest writes an audit_log entry with action='mcp_write' or action='endpoint_data_appended'. The endpoint detail page's Agent writes & ingest card shows the last 20 jobs across both kinds. Storage cap and per-plan file-size limits are enforced server-side before any bytes hit Neon or R2.

Limits today

  • Updates and deletes are by primary key only. No WHERE x = 'foo' mass updates. Limits blast radius.
  • No schema mods. Agents can't CREATE TABLE or ALTER. Use the dashboard or Neon SQL editor for those.
  • Per-table opt-in is not yet a thing. The toggle is per-endpoint. If you want writes on notes but not customers, use two endpoints.
  • Metadata tables are protected. pdf_metadata, audio_metadata, etc. are derived by the ingest pipeline and don't accept direct agent writes.