AI Agent Operational Manual

Publishing apps from your AI agent

Complete reference for Claude, Codex, ChatGPT, and any MCP-compatible client. Use the publishwith.ai MCP tool to create, update, and manage hosted apps.

1. Setup

Add the MCP server to your client once. Use the credentials from your dashboard.

Claude Desktop / Claude Code

claude mcp add publishwith --transport http "https://publishwith.ai/mcp" \
  -- --header "Authorization: Bearer YOUR_API_KEY"

Codex CLI

codex mcp add publishwith --transport http "https://publishwith.ai/mcp" \
  -- --header "Authorization: Bearer YOUR_API_KEY"

mcp.json / any client

{
  "publishwith": {
    "type": "http",
    "url": "https://publishwith.ai/mcp",
    "headers": { "Authorization": "Bearer YOUR_API_KEY" }
  }
}
ℹ️
The tool exposes a single publishwith action-based tool. All operations use action="..." as the first parameter.

2. Static apps — HTML & Markdown

Static apps serve files directly. Supported formats: HTML, Markdown (.md), CSS, JS, JSON, images, and any text format. Markdown files are automatically rendered to HTML with GFM support (tables, code blocks, task lists).

Minimal HTML app

create_app(name="my-report")
→ { app_id: "abc123", url: "https://publishwith.ai/a/my-report/" }

publish_static(app_id="abc123", files={
  "index.html": "<h1>Q3 Revenue</h1><p>$1.2M</p>"
})
→ { url: "https://publishwith.ai/a/my-report/", version: 1 }

Markdown app

publish_static(app_id="abc123", files={
  "index.md": "# Q3 Report\n\n| Month | Revenue |\n|---|---|\n| Jul | $380K |\n| Aug | $420K |\n"
})

Markdown is rendered server-side using GitHub Flavored Markdown. Tables, fenced code blocks, task lists, and inline HTML are all supported.

Multi-file app

publish_static(app_id="abc123", files={
  "index.html": "<!DOCTYPE html>...",
  "style.css":  "body { font-family: sans-serif; }",
  "data.json":  '{"revenue": 1200000}'
})

3. Dynamic apps — SQL + Liquid templates

Dynamic apps have an isolated SQLite database and Liquid templates that render data server-side. Routes can have parameters, queries, and POST handlers.

create_app(name="inventory", mode="dynamic")
→ { app_id: "xyz789" }

execute_sql(app_id="xyz789",
  sql="CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)")

execute_sql(app_id="xyz789",
  sql="INSERT INTO items (name, qty) VALUES ('Widget A', 42), ('Widget B', 7)")

publish_dynamic(app_id="xyz789",
  manifest="""
kind: mcp-html-app
version: 1
routes:
  - path: /
    template: index.html
    queries:
      items:
        sql: "SELECT * FROM items ORDER BY name"
""",
  templates={
    "index.html": """
<!DOCTYPE html>
<html><body>
<h1>Inventory</h1>
<ul>{% for item in items %}
  <li>{{ item.name }} — {{ item.qty }} units</li>
{% endfor %}</ul>
</body></html>
"""
  }
)
→ { url: "...", version: 1, routes: ["/"] }

Manifest schema

FieldRequiredDescription
kindYesMust be mcp-html-app
versionYesMust be 1
routes[].pathYesURL path, supports {param} placeholders
routes[].templateGET onlyLiquid template filename
routes[].methodNoPOST for mutation routes (default: GET)
routes[].queriesNoNamed SQL SELECT queries injected into template context

4. Incremental uploads

For large apps, upload files in batches using the token returned by the first call. All calls with the same token update the same version atomically.

# Start a new version — no token
publish_static(app_id="abc123", files={"index.html": "..."})
→ { token: "tok_abc...", version: 2 }

# Add more files to the same version
publish_static(app_id="abc123", token="tok_abc...", files={"style.css": "..."})
publish_static(app_id="abc123", token="tok_abc...", files={"app.js": "..."})
⚠️
Tokens expire after 30 minutes. Starting a new publish_static without a token always creates a new version.

5. Share links

Share links give unauthenticated access to a specific app. Access is tracked (anonymised — no IP or user agent stored).

create_share_link(app_id="abc123", ttl_hours=72)
→ { share_url: "https://publishwith.ai/p/abc...", expires_at: "..." }

list_share_links(app_id="abc123")
→ { links: [{ id: "lnk_...", share_url: "...", view_count: 14 }] }

revoke_share_link(link_id="lnk_...")
→ { revoked: true }

list_access_log(link_id="lnk_...")
→ { view_count: 14, events: [{ timestamp: "...", action: "view", path: "/" }] }

6. All actions reference

ActionDescription
create_appCreate a new app. Returns app_id and URL.
list_appsList all apps in your tenant.
delete_appDelete an app and all its data.
publish_staticUpload static files (HTML, MD, CSS, JS…). Returns URL and version.
publish_dynamicDeploy a dynamic app with manifest + Liquid templates.
validate_appValidate a manifest before publishing. Returns errors if any.
execute_sqlRun SQL on an app's isolated SQLite database.
previewFetch the rendered HTML of an app (dry-run for dynamic APIs).
create_share_linkCreate a public share link with optional TTL.
list_share_linksList share links for an app.
revoke_share_linkRevoke a share link immediately.
list_access_logView anonymised access events for a share link.
create_userInvite a user to your tenant.
list_groupsList access groups in your tenant.
docsFetch inline documentation. Use topic=X for a specific section.

7. Error handling

Every action returns a hint field on error. Always surface it to the user or use it to self-correct before retrying.

💡
Call action="docs" to fetch the latest in-context documentation at any time. Use topic="manifest", topic="sql", or topic="share" for filtered sections.

Common errors

ErrorCauseFix
app not foundWrong app_idCall list_apps to get valid IDs
token expiredUpload session > 30 minStart new session with publish_static (no token)
mode mismatchpublish_static on a dynamic appUse publish_dynamic
quota exceededPlan limit reachedDelete unused apps or upgrade plan
manifest invalidYAML/schema errorUse validate_app first to get field-level errors