developer tools

Drive your site from your agent.

pepita is a website publishing tool. Alongside the editor it gives you an MCP server, so you can create, edit, preview and publish a site without leaving your coding agent.

Setup · once

Connect and sign in

The connector signs in with your existing pepita account. Authorize a client once and it stays authorized until you revoke it in the editor under your avatar → Settings → Devices.

The MCP server

It speaks plain MCP, so any client that reads the protocol can use it. Also listed in the MCP Registry as dev.pepita.mcp/mcp, if your client browses the registry.

claude desktop/web claude code codex opencode

Settings → Connectors → Add → Browse connectors

Search for Pepita

$ claude mcp add --transport http pepita https://mcp.pepita.dev/mcp

$ codex mcp add pepita --url https://mcp.pepita.dev/mcp

$ codex mcp login pepita

# opencode.json

{

"mcp": {

"pepita": {

"type": "remote",

"url": "https://mcp.pepita.dev/mcp",

"enabled": true

}

}

}

 

$ opencode mcp auth pepita

Reference · every tool

Everything the connector can do

One row per tool, grouped by what it touches. Names and parameters are exactly what the connector answers with, so this doubles as the reference your client sees.

44 connector tools

Sites

list_sites()

The entry point — every other tool needs a slug from here.

create_site(name, allow_embedding?, block_ai_crawlers?)

A new site, live the moment it is made, with starter content in it. Shape it afterwards with write_site_file.

get_site_status(slug)

One site at a glance — unpublished changes, live URL, active preview links.

list_site_files(slug, state?, preview?)

The file tree: the working copy by default, or the live site, or one preview.

read_site_file(slug, path, state?, preview?)

One file's contents, plus a content sha so a later write can catch conflicts.

write_site_file(slug, path, content)

Create a file or replace one whole. Writes to the working copy — nothing is public until you publish.

edit_site_file(slug, path, old_str, new_str)

Change part of a file: replace one exact, unique snippet instead of resending the whole thing. Prefer it over write_site_file on a page that already exists — a full rewrite costs the entire file, and on a long one it can be cut off half-written.

delete_site_file(slug, path)

Remove a file from the working copy. Without it a file written under a mistyped name could only ever be overwritten, never taken away.

read_served_page(slug, path, state?)

What a VISITOR gets for one address — the response, not the file. On a pepita site the two differ enormously: a content tag is one line in the file and every item in the response, and an item's own address is in no file at all. Reads the unpublished working copy by default.

publish_site(slug)

Live at <slug>.pepita.page, and on any custom domain.

create_preview(slug)

A stable URL you can send to anyone, for review before going live.

update_preview(slug, name)

Same URL, new content — so a link you already sent shows your latest changes.

list_previews(slug)

Their names and URLs — the name is what the update and delete tools take.

delete_preview(slug, name)

The version itself is kept — restorable later from the editor's History.

Form data

get_form_records_count(slug)

A map of form name → how many submissions it holds. The cheap first look, before reading any of them.

list_form_records(slug, form_name, live?, preview?, confirm_large?)

One form's submissions, newest first. live or preview picks the source; neither means your own test submissions from the editor. Past 100 it answers with the count instead, so your agent can ask before filling the screen — then call again with confirm_large.

get_form_export_url(slug, form_name, format?, live?, preview?)

A time-limited download link — xlsx, csv or json — so your agent can hand you a file instead of reading a thousand rows out loud.

Confirmation emails

list_email_templates(slug)

One per form, each matched to the form by name.

read_email_template(slug, form_name)

Subject, sender, HTML body, and the sha you need to change it.

write_email_template(slug, form_name, html?, subject?, from?, from_name?)

Creates the template when the form has none and updates it when it does. Leave out html and pepita seeds a starter body. Updating an existing template changes the working copy — save it to change the emails people receive. A brand-new template is saved as it is created.

save_email_template(slug, form_name)

The same act as the editor's Save button on the template's row. Editing a template anywhere changes only the working copy; this is what makes those edits the email a visitor actually gets.

delete_email_template(slug, form_name)

Confirmation emails for that form stop going out immediately.

list_email_template_images(slug, form_name)

The names to write in the body — as bare filenames, not as the URL beside them. pepita expands a filename at send time onto whichever of your domains matches the address the mail goes out from; a URL written in by hand is left exactly as it is, forever.

delete_email_template_image(slug, form_name, name)

Emails sent from now on show a gap where it was. Mail already delivered may keep showing it for a long time — a picture in someone's inbox is a copy, and deleting it here cannot reach that.

Content collections

Repeating content — a blog, a menu, a team page — as a template describing one item plus the items filling it. The name ties them together, and a page shows the collection with a <pepita-content> tag you write like any other markup. A collection can have several layouts: every template command below takes an optional layout name, and leaving it off means the default one — which is the template itself.

list_content_templates(slug)

The starting point — the names here are the names a page asks for.

read_content_template(slug, name, layout?)

The complete HTML document for one item — markup only. A <style> or <script> inside it is dropped; those live in the collection's own two files.

write_content_template(slug, name, html, layout?)

A brand-new template is saved as it is created; changing one that exists writes a working copy.

save_content_template(slug, name, layout?)

A collection's parts are edited together, so one save promotes them together. Nothing pending is a normal outcome, not a failure; a partial save reports what did not land.

delete_content_template(slug, name, layout?)

With a layout it removes only that shape; without one, the template and every layout with it.

read_content_file(slug, name, file)

A collection's own style.css or script.js. Read before you write: writing replaces the file outright.

write_content_file(slug, name, file, text)

Where a collection's look and behaviour live — one of each per collection, applying to every layout. A <style> or <script> written inside a template is dropped silently, so this is the only place either belongs. Changes the working copy; save_content_template with no layout promotes it.

list_content_records(slug, name, oldest?)

Newest first, each with its id, its address and whether it is live or still a draft.

add_content_records(slug, name, records)

All of them land or none do — a refusal names every bad item by its position.

update_content_record(slug, name, id, data)

The published state is unchanged — editing never publishes or unpublishes.

delete_content_record(slug, name, id)

One item. The collection and its template are untouched.

publish_content_record(slug, name, id)

A new item is a DRAFT — it shows in the editor and on preview links until this is called.

unpublish_content_record(slug, name, id)

Nothing is deleted — the item stays in the editor.

add_content_image(slug, name, url)

Fetches a picture from a public address into the collection and hands back the pepita url to put in the item.

get_content_image_upload_url(slug, name)

A short-lived address that takes one file with no other credential, for an agent that can run a command.

Hosted video

list_videos(slug)

Each video with its streaming (HLS) URL and poster. Videos live outside the file tree, so this is how you find them.

get_video_original_url(slug, id)

A temporary link to the original file you sent, before streaming prep.

rename_video(slug, id, new_name)

Its URLs never change, so pages that reference it keep working.

delete_video(slug, id)

Removes the video for good, with its streaming files and poster.

Platform contract

get_building_guide(topic)

The exact contract for overview, forms, confirmation-emails, video or headers-and-csp. The only tool that takes no slug, and the one to call before writing any of those — a guessed form action produces a page that looks right and silently misses the feature.

Your device

sign in and out()

The connector signs itself in over OAuth the first time you use it — there is no key to paste. Every authorized client is listed in the editor under your avatar → Settings → Devices.

Coming soon: custom domains, site health.

Try these

List my pepita sites and show me the files in <slug>.

On <slug>, rewrite the About section to be warmer.

Add a contact form to <slug> and write the confirmation email it sends back.

Give me a shareable preview link for <slug> to send my client.

Publish <slug> to the live site.

Forms · built in

Forms and confirmation emails

A pepita site collects form submissions with no backend to build and no third-party form service. Point a plain HTML form at pepita and every submission is stored, readable from the connector or from the editor's Forms panel. Add an email template and pepita also emails the person back.

The form

Two things make it work: the action, and a hidden _form field naming the form. Every other named input becomes a field of the submission, and its name becomes a column in the editor’s Forms panel.

contact.html

<form method="post" action="/__pepita/forms">

<input type="hidden" name="_form" value="contact">

<input type="email" name="email" required>

<textarea name="message"></textarea>

<button>Send</button>

</form>

Write the real action and nothing around it — pepita injects its own spam check and delivery token into any form pointed there, so never add hidden fields of your own beginning with _. Optionally, <input type="hidden" name="_redirect" value="/thanks.html"> sends the visitor to your own page afterwards instead of pepita's built-in one.

The confirmation email

A form emails the person who filled it out only if it has a template — an HTML email body plus a subject and a sender, matched to the form by name. Write one with write_email_template, or by hand in the editor's Forms panel, in that form's own block. {{field}} placeholders in the subject and the body fill from the submission, so Thanks {{name}}! works. Deleting the template turns the email back off.

The name is the whole link between the two, so it is worth seeing twice. The form above declares _form="contact"; the template that answers it is called contact as well, and that is the entire wiring — there is no id to copy and nothing to connect in a settings screen.

the matching template

# the form said: name="_form" value="contact"

write_email_template(slug, form_name="contact", subject="Thanks, {{name}}!",

from="hello", from_name="Acme", html=…)

save_email_template(slug, form_name="contact")

Editing a template does not change what people receive — saving it does. A template has a working copy, like the site itself: every edit is kept, from any tool, and the emails going out keep using the last saved version until you say otherwise. So you can rewrite a confirmation email in the middle of the afternoon without a visitor receiving the half-finished draft. Save it with save_email_template, or the Save button on the template's row in the editor, where a dot marks anything unsaved. Creating a brand-new template is the one exception: it is saved as it is created, because there is no earlier version for it to be pending against.
The recipient is always the field named email. Pepita reads the address from the submission's email field, every time. So a form that should send a confirmation must carry an input named exactly email (a visible one, or a hidden input your script fills). A submission without it has nobody to write to, and the email simply doesn't go out. The editor's Forms panel flags both cases on the form itself.
Sends are capped per site — 10 a minute, 50 a day — as an abuse guard. Anything over the cap is recorded as blocked rather than sent, and shows that way in the Forms panel next to the entry it belonged to.

Pictures in the email

A template carries its own pictures. Drop a JPEG or PNG onto that form's block in the editor's Forms panel. Reference one by its bare filename<img src="hero.jpg"> — and pepita expands it to a full URL as the email goes out, on whichever of your domains matches the address it is sent from. That is also why adding a custom domain later doesn't break every picture in the template: the body never held a hostname to go stale.

A picture hosted anywhere else needs its full https:// address. A relative one resolves against nothing in an inbox — there is no page for it to be relative to — so it is broken there even when a preview looks right. The bare filename above is the exception, and only because pepita expands it for you before sending.

JPEG and PNG only, decided by the file's contents rather than its name. SVG is refused because mail clients block it, and WebP because support is still patchy exactly where it would matter. Up to 300 KB a picture, and 1 MB or ten pictures a template — an email is read on a phone, often on a slow connection.

Submissions are view-only — pepita stores and shows them, and the confirmation email is the one thing it sends on your behalf. For the full contract, including what the Forms panel's source filter does with test submissions, ask the connector for get_building_guide with topic forms or confirmation-emails.

Images · automatic

Images optimise themselves

When you publish, pepita transcodes every jpg, jpeg and png into a smaller WebP beside it: hero.jpg gets a hero.jpg.webp. Your original is never touched — not moved, not re-encoded, not replaced. There is nothing to configure. The only thing to know is which of the two URLs to point at.

The two URLs

hero.jpgThe original, byte for byte, forever. Point anything that leaves your site at this one.original
hero.jpg.webpThe optimised copy — often a fraction of the size. Point your site's own markup at this one.optimised
index.html

<!-- your own pages: use the optimised copy -->

<img src="/hero.jpg.webp" alt="…">

<style> .hero { background-image: url(/hero.jpg.webp); } </style>

 

<!-- anything that leaves the site: use the ORIGINAL -->

<meta property="og:image" content="https://you.pepita.page/hero.jpg">

Referencing the .webp cannot break your site. If the optimised copy doesn't exist — a brand-new image, one that didn't come out any smaller, or a site you haven't published since adding it — the server hands back the original's bytes, with the original's content type. So it is safe to write the .webp path before it exists. The worst case is a missed optimisation, never a broken image.
But use the original for anything that leaves the site. Social preview tags (og:image), email, and any URL someone might paste elsewhere should point at hero.jpg. WebP support in link-preview scrapers and mail clients is still patchy — and a missing social card is the kind of thing you only discover after posting. The original's URL is a permanent public contract: it will always serve exactly the file you uploaded.

The small print

jpg · jpeg · pngThe only formats transcoded. svg, gif, ico and files that are already webp are served untouched.
only if smallerSome images — flat-colour PNGs especially — come out bigger as WebP. Those get no copy at all, and the fallback quietly serves the original.
2560px long edgeAnything bigger is scaled down to fit, aspect ratio preserved, never scaled up. This, not the format change, is where most of the saving comes from.
your file winsUpload your own hero.jpg.webp and pepita leaves it exactly as you made it. It never writes over a file you put there.
Prefer the original everywhere? If quality matters more than bytes — you're publishing photography, say — just point your markup at hero.jpg and ignore the copy. Nothing obliges you to use it, for one image or across the whole site. (If pepita's AI is writing your pages, tell it so and it will do the same.)

Ship a site in seconds. Change it from anywhere.