@zitegen/mcp
v0.0.31
Published
MCP server for Zitegen — manage AI-generated websites from Claude Code, Cursor, or any MCP-compatible tool.
Readme
@zitegen/mcp
An MCP (Model Context Protocol) server for Zitegen — manage your AI-generated websites from Claude Code, Cursor, or any MCP-compatible tool.
Setup
MCP access requires a Pro plan or above (Free-tier accounts can't generate an API key).
- Generate an API key in the Zitegen dashboard: Settings → API keys → Generate key.
- Add the server to your MCP client:
claude mcp add zitegen -e ZITEGEN_API_KEY=zit_... -- npx @zitegen/mcp(For Cursor or another MCP client, point it at npx @zitegen/mcp with the same
ZITEGEN_API_KEY environment variable.)
By default this talks to http://localhost:3000 (the dashboard dev server). Set
ZITEGEN_API_URL to point at a deployed Zitegen instance instead.
Tools
list_sites— list all your sites."What sites do I have in Zitegen?"
generate_site— generate a new site from a prompt (consumes 10 credits). Passlocales(e.g.["fr", "pt-br"]) to also translate the new home page into those locales right away — one translate-page credit per locale, each attempted independently so one failure doesn't affect the site itself or the other locales."Generate a Zitegen site for a portfolio for a photographer named Alex." "Generate a Zitegen site for a bakery, and translate the homepage into French and Spanish."
list_pages— list a site's pages (pageId, path, title, locale, translationGroupId, status). The cheap way to get a pageId — unlikeget_files, it doesn't compile the project's Astro/CSS output first. Every page has alocaleand atranslationGroupId, even on a single-locale site; passlocaleto only list pages in one locale.statusis "draft" or "published" — a draft page never appears in any real deploy until published. Never includes a content type's detail-page template — seemanage_templatesfor those."What pages does my portfolio site have?" "What French pages does my portfolio site have?"
get_page_tree— get one page's raw, editable component tree — the same shapepush_filesexpects back — plus itslocale/translationGroupId/status. Use this (notget_files) before hand-editing a page. Not for content-type templates — those aren't ordinary pages; usemanage_templatesinstead."Show me the component tree for my portfolio site's homepage."
get_files— get a site's compiled Astro project files (every page, fully compiled Astro/CSS/config). Costs more per call thanlist_pages/get_page_tree— prefer those when you just need a pageId or one page's editable tree."Show me the files for my portfolio site."
push_files— replace one page's component tree with edited JSON. This is a full-tree replacement, not a patch to compiled.astrosource — fetch the tree viaget_page_treefirst, edit it, then push it back. Style nodes with Tailwind utility classes inprops.className(this is how AI-generated content is styled); leavestylesempty unless you need something Tailwind can't express. For a small or mechanical edit, preferpatch_page— it costs a lot less for a page with many nodes."Update the hero heading on my portfolio site's homepage to 'Alex Kim, Photographer'."
patch_page— apply a small batch of targeted edits (insert/update/replace/delete by node id) without fetching or resending the whole tree — the cheap alternative toget_page_tree+push_filesfor adding a field, tweaking a color, or removing a section. Node ids come from a priorget_page_tree/push_files/generate_siteresponse; batch several ops into one call rather than one call per change."Add a subject field and a message field to the contact form on my portfolio site."
convert_to_shared_component— turn an existing node into a site-scoped Reusable Component (e.g. a Header or Footer shared across pages) — the same action as the canvas's "Convert to reusable component" menu item. Neitherpush_filesnorai_promptdoes this on their own; call this afterward on the node they created."Turn the header I just added into a reusable component."
list_shared_components— list a site's Reusable Components (id, name, rootType, pageCount). Use this to find an existing one before creating a duplicate withconvert_to_shared_component, or to get the idattach_shared_componentneeds. Doesn't include each definition's full tree — read it viaget_page_treeon any page that already uses it (its instance node carries the same id this tool returns)."What reusable components does my portfolio site have?"
attach_shared_component— add an existing Reusable Component onto a different page as a new instance (appended as the last child of that page's root). This is the reuse-elsewhere halfconvert_to_shared_componentdoesn't do on its own."Add the Footer component to my portfolio site's About page too."
preview_site— render a site's current state via a realastro buildand deploy it to the preview branch, without touching production. Use this to verify a real build succeeds before callingpublish_site— same idea asgit pushto a branch before merging. Shares a combined 150/day rate limit withpublish_site(see below)."Preview my portfolio site before we publish it."
publish_site— render a site's current state via a realastro buildand publish it to production on Cloudflare Pages. Limited to 150 real builds/day combined withpreview_site— generous for normal use, but a guardrail against a runaway script or agent loop."Publish my portfolio site."
ai_prompt— send a natural-language editing instruction to a page and save the result (consumes 3 credits). Unlikepush_files, you don't need to fetch or hand-edit the tree yourself — the AI does that server-side, the same way the dashboard's chat panel does. The response'stouchedNodeIdis the node it just created/replaced (ornull), for chaining intoconvert_to_shared_component. It has no concept of Reusable Components itself — "create a footer as a component" just adds a normal footer.pageIdalso accepts a content-type detail-page template's id (get one viamanage_templates, notlist_pages) — the AI is told the edit applies across every entry of that content type, so keep the instruction specific."On my portfolio site's homepage, change the hero heading to 'Alex Kim, Photographer'."
manage_domain— connect, check, or remove a site's custom domain.connectattaches it to the site's Cloudflare Pages project (add a CNAME at your DNS provider pointing at the site's*.pages.devhost — Cloudflare verifies and issues SSL automatically);statusre-checks live verification/SSL;removedetaches it."Connect www.alexkimphoto.com to my portfolio site."
manage_site_settings— get or update a site's basic settings: display name, SEO title/description, the<title>tag template (e.g."%site% | %page%"to put the brand first; must include%page%), language/locales, favicon, heading/body fonts, social links, Open Graph title/description/image, contact email, Design Guide text, color palette, custom brand colors, and the "Built with Zitegen" badge toggle.action: "get"returns current values;action: "update"applies a partial patch (only pass the fields you want to change). Does NOT cover analytics/tracking IDs, custom head/body code, or third-party integrations (Stripe, Mailchimp, CAPTCHA/spam-protection keys, etc.) — manage those from the dashboard's Site Settings / Integrations panels."Set my portfolio site's SEO title to 'Alex Kim Photography — Portfolio'." "What's the current Design Guide for my portfolio site?"
manage_page_seo— get or update ONE page's SEO fields: meta title/description, canonical URL, noindex, OG image, structured-data (JSON-LD) type/fields, and focus keyword. Per-page metadata, distinct frommanage_site_settings' site-wide defaults — a page's own value here overrides those when set, and falls back to them when left blank.pageIdcomes from a priorlist_pagescall.action: "get"returns current values;action: "update"applies a partial patch.schemaType/schemaDataare validated server-side against the real per-type shape (12 types: none/article/faq/product/organization/recipe/howto/video/review/ event/course/book), so a bad payload fails with a clear error rather than being silently dropped."Set the focus keyword on my portfolio site's homepage to 'wedding photographer chicago'." "Add FAQ structured data to my pricing page with these three questions..."
manage_articles— manage a site's blog articles AND every user-defined content type's entries (Phase 4 CMS, separate from the page treeget_files/push_filesedit).contentType(defaults"blog") selects which type an action applies to — set it to amanage_content_typesslug to work with that type's entries instead. Every article/entry has alocaleand atranslationGroupId, same as pages — passlocaletolistto filter by it, or tocreate(defaults"en") to author a standalone entry tagged with that locale (not accepted onupdate— locale can't change after creation, same ascontentType). Actions:list,get,create,update(any subset of fields — settingstatusto"published"is how you publish/unpublish, there's no separate action for it),delete,generate(blog-only: atopicstring in, a{title, excerpt, body}draft out — not saved, follow up withcreate/updateyourself).bodyis a structured doc (the same tiptap/ProseMirror JSON shape the dashboard's rich-text editor produces), not markdown — see the tool's own description for a worked example.create/updatealso take metadata fields — author, category, tags,terms(the general form of category/tags and the only way to assign a custom taxonomy — seemanage_taxonomies;{ [taxonomySlug]: string[] }, each name auto-resolved or created), featured, showToc, socialImageUrl, series/seriesPart, section/sortOrder, readingTime, canonicalUrl, publishedAt, contentUpdatedAt — mirroring the dashboard editor's Settings tab.schemaType(none/article/faq/recipe/howto/ video/review/event/course/book) plus a matchingschemaDataobject (e.g. FAQ'squestions, Recipe'singredients/instructions) drive the entry's structured data (JSON-LD) — see the tool's own description for the exact per-type shape. For a non-"blog"contentType, also setcustomFields({ [fieldKey]: value }matching that type's field definitions). Entries compile into code download, this tool's own reads, and the live published site."Create a draft blog post on my portfolio site titled 'Behind the Shoot: Golden Hour Portraits'." "Generate a draft blog post about our new golden hour portrait package, then publish it."
manage_content_types— define user-defined content types (e.g. "Recipes", "Team Members") alongside the built-in"blog"type, each with its own custom field schema. A type'sslugbecomes both its URL prefix (/recipes/*) and thecontentTypevaluemanage_articlesuses for its entries. Actions:list,get,create(requiresname,slug, andfields— at least one, each withfieldKey/label/fieldType— one oftext/textarea/richtext/number/boolean/date/url/image/select/tags—required, and (forselect)options),update(any subset —fieldsREPLACES the whole list, it doesn't merge),delete(irreversible, also deletes every entry of that type).status("draft" or "published", defaults "published" on create) is settable on bothcreateandupdate— a draft type's entire/<slug>/*route, and every one of its entries regardless of that entry's own status, is hidden from every real deploy until the type itself is published."Create a Recipes content type with fields for servings (number), vegan (boolean), and source URL, then add a Banana Bread recipe."
manage_taxonomies— manage the categorization system behindmanage_articles'category/tagsfields, extensible with custom taxonomies. Every site automatically has a"category"(hierarchical — terms can have a parent) and"tags"(flat) taxonomy; you only create TERMS for those, not the taxonomies themselves. A custom taxonomy you create (e.g."Region") applies to every content type on the site, blog included — there's no per-type scoping. Actions:list,get(returns the full term list),create(name,slug,hierarchicaldefaults false),update,delete(irreversible, owner-only, cascades every term + assignment —"category"/"tags"can never be deleted, and their slug can never change). Term actions — usually unnecessary, sincemanage_articles'termsfield creates a term automatically just by naming it —createTerm(useful to pre-seed a fixed vocabulary before anything uses it),updateTerm,deleteTerm."Create a 'Region' taxonomy for my site and tag the Golden Hour article with 'North America'." "What categories does my blog currently use?"
manage_templates— view and customize a content type's detail-page template — the shared layout every entry of that type renders through, bound with{{fieldKey}}placeholders and two marker nodes (data-zg-article-contentfor the entry body,data-zg-article-tocfor its table of contents). A template is a real, editable component tree but not an ordinary page — it never appears vialist_pages/get_page_tree/push_files/patch_page. A content type can have several saved variants with exactly one ACTIVE per (content type, locale); the universal variant used when no locale-specific one exists is alwaystemplateLocale: "default". Actions:list(requirescontentTypeSlug),get(requirespageId— full tree),create(requirescontentTypeSlug; omitlocaleto get-or-create the default variant, or pass it to create a new active variant for that locale),rename(requirespageId/title),setActive(requirespageId),duplicate(requirespageId),delete(requirespageId, irreversible — deleting the active variant just falls back to the default template, or plain rendering if the default has none active either),updateTree(requirespageId/tree— full replace, likepush_files),patchTree(requirespageId/ops— targeted edits, likepatch_page). Keep the marker nodes and{{fieldKey}}placeholders intact when editing the tree, or every entry of that type silently loses its body/table of contents rendering, not just one. For a natural-language edit instead of hand-editing the tree, pass a template'spageIdtoai_prompt."Show me the blog post template for my portfolio site." "Create a French variant of the blog post template with a different hero layout."
list_leads— list submissions to a site's forms. Any<form>element in the component tree (seeget_page_tree/push_files) is auto-detected and captured with no extra wiring — there's no separate form-builder step, and noaction/backend to set up. Newest first, 50 per page (page, 0-indexed). PassformId(aleads[].formIdfrom a prior call) to filter to one form, which also returnsfieldColumns— that form's real field names (fromprops.nameon its inputs) — soleads[].datacan be read by field name instead of an opaque blob. Read-only — deleting an individual submission or a whole form is dashboard-only (the Leads panel), not available via this tool."List the most recent leads on my portfolio site's contact form."
manage_form— get or set a form's visitor-facing submit outcome, upload limits, and an outbound webhook: the success/error message text, an optional redirect URL to send the visitor to instead of an inline success message,maxFiles/maxFileSizeMboverrides that TIGHTEN (never loosen) the platform's default upload limits (5 files/submission, 10MB/image or document, 50MB/video) for this specific form, andwebhookUrl— a public http(s) URL that gets a fire-and-forget POST of{event, siteId, formId, leadId, path, submittedAt, data}on every submission (a delivery failure never blocks or fails the submission itself; a private/internal address is silently rejected). Read live at submit time, so a change takes effect on the very next submission with no republish needed."list"returns every form the site has captured a submission for;"get"fetches one byformId(aleads[].formIdfromlist_leads);"update"applies a partial patch — pass an empty string to clear a text field (includingwebhookUrl) back to the default, ornullto clearmaxFiles/maxFileSizeMbback to the platform default. This customizes what the visitor experiences submitting the form, not the form itself (still authored viapush_files/patch_page/ai_prompt) or its confirmation-email toggle (dashboard-only) — and the accepted file types themselves stay fixed platform-wide."Set a custom thank-you message on my contact form and redirect visitors to /thank-you after they submit." "Limit my résumé-upload form to a single 5MB PDF." "Send every submission on my waitlist form to my Zapier webhook at https://hooks.zapier.com/hooks/catch/123/abc."
The same tool also manages up to 5 custom, conditional admin-notification emails per form — an additional, independently-routed layer on top of the always-on default owner notification, never a replacement for it (e.g. "also email sales@ when the budget field contains 10000", on top of whatever always goes to the site's own contact address).
"addNotification"(name,sendToEmail,subject,bodyrequired;enableddefaultstrue,conditionsdefaults to none/always-send),"updateNotification"(notificationIdrequired, everything else a partial patch),"removeNotification"(notificationIdonly) —notificationIdcomes from anotifications[].idin this tool's own list/get/update result.subject/bodysupport merge tags:{all_data}renders every submitted field askey: valuelines,{field.fieldName}renders one field's value. Each condition compares one submitted field againstequals/contains/notEquals(all requirevalue) orisEmpty/isNotEmpty(novalue); all of a notification's conditions must match for it to fire (AND only, up to 5 — no OR/nested groups). A disabled notification is kept but never sent. This is dashboard-editable too, in the Leads panel — plain fields, not a visual rule-builder — either surface works interchangeably."Add a notification on my quote-request form that emails [email protected] when the budget field contains '10000' or more." "Turn off the shipping-delay notification on my order form for now, but don't delete it."
Resources
zitegen://sites/{siteId}/project— the full compiled Astro project for a site (package.json,astro.config.mjs, everysrc/*.astropage and component, public assets, SEO files — the same outputget_filesreturns), exposed as an MCP resource so a client can attach the whole project as ambient context instead of calling a tool. Discoverable viaresources/list(lists one entry per site you own).
Adding a modal via push_files
A popup (signup form, announcement, pricing detail, etc.) is a "dialog" node — it compiles
to a real native <dialog> element, and Zitegen's own client script wires up opening it,
closing it on a backdrop click, and closing it on Escape. You only need to author two linked
nodes anywhere in the tree you push:
- A trigger — any node (typically
"button") withprops["data-zg-modal-open"]set to a short name you choose, e.g."signup". - The dialog itself — a
"dialog"node withprops["data-zg-modal"]set to that same name. This name is unrelated to the node's"id"— never link them by id, since ids get reassigned on save. - Give the dialog one inner wrapper child holding its real content, and optionally a button
with
props["data-zg-modal-close"]: truefor an explicit close control. - Never set
props.openon the dialog, and never put a display-related class ("hidden"/"flex"/"block") on it — its closed-by-default state is handled natively by the browser, not by Zitegen's styling system. - For a dialog that should open itself instead of (or in addition to) a trigger — e.g. "show a
promo popup 3 seconds after the page loads" — set
props["data-zg-modal-delay"]to the delay in milliseconds as a string, e.g."3000". It auto-shows on every eligible page load; there's no "only show once per visitor" memory. - To show a modal on some pages but not others (e.g. every page except Contact), just don't push that dialog/trigger pair into the excluded page's tree — there's no separate targeting field.
This works identically whether the site is live-deployed or downloaded as a standalone Astro
project via get_files — the modal script ships in both.
Building a multi-step form via push_files
A "form" node can be split into steps that show one at a time — no separate tool, this is
authored directly in the tree you push. Same marker-attribute convention as the modal above
(nothing in the component-model schema changes):
- A step is any descendant of the
"form"node — typically a"div"— carryingprops["data-zg-form-step"]: true. Steps are shown in the order they appear in the tree, not by an index you set. - A "Next" control is a
"button"withprops.type = "button"(never"submit"— that would trigger a real submission) carryingprops["data-zg-form-step-next"]: true. A "Back" control is the same shape withprops["data-zg-form-step-prev"]: true. - The real submit button (
props.type = "submit", no step-next/prev props) goes only inside the last step — it's simply not interactable until a visitor reaches it. - Zitegen's own client script handles step show/hide and validates only the current step's
fields before letting a visitor advance (native HTML validation —
props.required,type="email", etc. — still works exactly as you'd author it on any input).
Optionally, add a progress indicator — a container outside every step (a sibling of them,
e.g. a direct child of the "form") with props["data-zg-form-progress"]: true. The platform
sets a "--zg-progress" CSS variable (0 to 1) on it every time the visitor moves between steps,
so a child element can fill a bar with a Tailwind arbitrary width like
"w-[calc(var(--zg-progress)*100%)]". For a text counter, give any element
props["data-zg-form-progress-current"], "-total", or "-percent": true — its text is
replaced with the live current step number (1-based), total step count, or percent complete;
author sensible fallback content (e.g. "1") so it still reads correctly before scripts run.
For discrete step dots instead of (or alongside) a bar/counter, use a separate container —
same outside-every-step placement — with props["data-zg-form-progress-dots"]: true. Give it
exactly ONE child element with props["data-zg-form-progress-dot"]: true: the platform
clones that single dot once per step automatically at runtime, so never author more than one dot
by hand even for a 5-step form. Style the dot with a data-zg-dot-state attribute selector —
e.g. Tailwind's "data-[zg-dot-state=current]:bg-blue-600 data-[zg-dot-state=done]:bg-blue-300" —
the platform sets that attribute to "upcoming", "current", or "done" per dot on every step
change.
This works identically whether the site is live-deployed or downloaded as a standalone Astro
project via get_files — the multi-step script ships in both.
