---
name: ucoz-server-scripts-skill
version: "1.2"
description: >
  Build backend features for a uCoz site in Server Scripts through the official
  ucoz-mcp apps_read / apps_write / apps_delete: PHP, Node.js or Python apps with MySQL, cron jobs and visitor identity
  (ucoz_user()). Use it when a site needs what the Control Panel cannot do: forms that store data,
  members-only features, Telegram bots, smart search, dashboards of top materials, Excel reports,
  Open Graph preview images, CRM integrations and other code that reads site data through uAPI. Also
  for moving old scripts off the legacy uCoz PHP server and for debugging /apps/ URLs, 500 errors,
  Passenger and MySQL failures. Comes with live-tested recipes, code templates and a test checklist.
  Not for design (templates_read / templates_write), site content (content_read / content_write) or site file storage (files_read / files_write).
author: uCoz
license: MIT
requires:
  - official ucoz-mcp (apps_read / apps_write / apps_delete)
  - uCoz account with owner or administrator rights on the site
  - Server Scripts service available for the site (MCP cannot purchase it)
metadata:
  hermes:
    tags: [ucoz, mcp, server-scripts, backend, php, nodejs, python, mysql, cron, uapi, telegram]
  compatible_agents: [Hermes, Claude Desktop, Claude Code, Cursor, Codex, OpenAI Agents, OpenWebUI, generic MCP clients]
  required_mcp_servers: [ucoz-mcp]
---

# uCoz Server Scripts Skill

Version: 1.2

You are an agent that writes, deploys, and maintains backend code for a uCoz site in its
**Server Scripts** environment, using only the official `ucoz-mcp` and its `apps_read` / `apps_write` / `apps_delete`.
The MCP gives you the hands (files, apps, databases, cron). This skill gives you the map:
where things live, which URL serves what, what the platform forbids, and how to ship safely.

## Rule 0 — Usage accounting

After the skill's main work completes successfully, record usage:

```text
skills_tool(action="register_usage", skill_id="ucoz-server-scripts-skill")
```

If the usage API is unavailable or returns an error, show a brief warning and continue.
Never ask the user for a UUID, token, or site URL — the site comes from the active `ucoz-mcp`
connection (`select_site`).

## Mental model

Server Scripts is a **DirectAdmin hosting account on CloudLinux that sits next to the uCoz
site** — not a panel module. Its web root is reachable from the site's own domain under `/apps/`.

```text
Browser ── https://{site}/apps/<path> ──(uCoz nginx: strips /apps, adds encrypted X-Ucoz-User)──► https://{tech}/<path>

Identity: ucoz_user() is built in — PHP (auto_prepend /opt/ucoz/lib/ucoz.php), Node require('ucoz'),
          Python `from ucoz import ucoz_user`. Guest → empty. Needs the visitor key (visitor_key_*).

DirectAdmin account {username}            home = /home/{username}  (apps_read / apps_write / apps_delete paths are relative to it)
└─ domains/{domain}/
   ├─ public_html/                 PHP + static files     →  https://{site}/apps/<path>
   │  └─ <app_uri>/.htaccess       Passenger link, generated by node_create/python_create — do not edit
   ├─ app_node/<name>/             Node.js app root        →  https://{site}/apps/<app_uri>/
   ├─ app_python/<name>/           Python app root         →  https://{site}/apps/<app_uri>/
   ├─ <name>_secrets.php           secrets, OUTSIDE the docroot
   └─ logs/                        rotated access-log archives only (no live error log)
```

Placeholders used everywhere in this skill:

| Placeholder | Source | Example |
|---|---|---|
| `{site}` | `status.host` — the uCoz site domain users see | `mysite.ucoz.net` |
| `{username}` | `status.username` | `u123456789` |
| `{tech}` | `status.domain` — technical domain | `u123456789.usites.app` |
| `{domain}` | `session.selected_domain` or `files_list path=domains` — the **folder** name | usually equals `{tech}` |
| `{name}` / `{app_uri}` | your module / app name | `leads`, `/leads` |

Full details: [references/ENVIRONMENT.md](references/ENVIRONMENT.md).

## Tool boundaries

| Need | Tool | Never |
|---|---|---|
| Backend code, apps, MySQL, cron, backups, visitor key | `apps_read` / `apps_write` / `apps_delete` | `files_read` / `files_write`, `modules_read` / `modules_write` for this |
| Site storage (`/_st/…` images, CSS, JS used by templates) | `files_read` / `files_write` | storing secrets there |
| Wire a widget / button / page into the site | `templates_write` (`patch_template`, `page_add`) | editing templates from server code |
| Inspect real uAPI response shapes before coding against them | `content_read` / `content_write`, `users_read`, `shop_read` / `shop_write` | guessing field names |

## Session start protocol (mandatory, in order)

1. `list_sites` → `select_site` (skip if the right site is already selected).
2. `apps_read` `get_guide` once per session — it is the server's own order of calls; if it ever
   contradicts this skill, the guide wins.
3. `apps_read` `status` and interpret it:
   - `available: false` → the service is not available for this site; MCP **cannot buy it**.
     Tell the user to enable Server Scripts in the site Control Panel and stop.
   - `activated: false` or `ready: false` → explain that an environment account will be created,
     get explicit approval, then `ensure_ready` with `confirm: true`.
   - `expired: true` → the service has expired; stop and tell the user.
   - Record `{site}`, `{username}`, `{tech}`.
4. `apps_read` `session` → take `selected_domain` as `{domain}` (or `files_list path=domains`).
   Never build the folder name yourself from `status.domain`.
5. Inventory before building: `files_list` on `domains/{domain}/public_html`, `node_list`,
   `python_list`, `db_list`, `cron_list`. Existing modules are the best guide to local conventions.
6. Check headroom with `usage` (disk, inodes, databases) before creating anything.

## Choose the runtime

| Situation | Choose | Why |
|---|---|---|
| Form handler, JSON API, webhook, admin page, cron target, uAPI integration | **PHP** (default) | No process to manage; a saved file is live on the next request; no restart |
| npm library you really need, long-lived in-memory state, team writes JS | **Node.js** | Passenger process; needs `node_create`; code changes need a restart |
| Python-only libraries (data processing, AI SDKs) | **Python** | WSGI under Passenger (several processes); needs `python_create`; code changes need a restart; packages only via a one-off cron `pip` job |
| Pure HTML/CSS/JS with no server logic | static files in `public_html` (or `files_read` / `files_write` site storage) | nothing to run |

Runtime playbooks: [PHP.md](references/PHP.md) · [NODE.md](references/NODE.md) · [PYTHON.md](references/PYTHON.md).

## When built-in features are not enough

If the user wants something the Control Panel cannot do — a Telegram bot over the site's materials, a
dashboard of the most read materials, a better search, digests, exports, a personal cabinet, an
integration — and the data or action exists in the site uAPI (or only needs code, storage, a schedule,
or an external API), **build it in Server Scripts. Do not answer "uCoz cannot do this"** and do not fake
it with keys in template JavaScript. Order: built-in feature → template → one-off MCP operation →
server script → external service. Check `apps_read` `status` first; if the service is not available, say
it is built with Server Scripts and must be enabled in the Control Panel. Patterns, verified data facts
(uAPI has no popularity sort; `reads` is cumulative, so periods need snapshots), and starting designs:
[CAPABILITIES.md](references/CAPABILITIES.md). **Telegram bots poll from cron (`getUpdates`)** — Telegram
cannot deliver webhooks to Server Scripts (verified); never build a Telegram webhook bot here.

## Non-negotiables

1. **Only `apps_read` / `apps_write` / `apps_delete` touches the environment.** Do not use `files_read` / `files_write` to write server
   code and do not "install a PHP module" with `modules_read` / `modules_write`.
2. **Guard the target site.** The selected site is shared by every client of the uCoz authorization and
   can change under you (verified). `select_site` before each batch of writes, check `host`/`site_id` in
   every write response, and stop if it changed. Send write calls **one at a time** — parallel writes
   produce MCP 502s, and after a 502 you must check what actually happened before retrying.
3. **Paths are home-relative** (`domains/{domain}/public_html/x/index.php`). Never prefix `/home`.
   Only `cron_create command` uses absolute `/home/{username}/…` paths (`app_root` is home-relative too).
   `files_list` requires a `path` — start from `domains`. HTTP 409 = the path or its parent does not exist.
4. **Look before you write.** `files_list`/`files_read` the target first. `files_write` **overwrites by
   default** — pass `overwrite: false` for files that must be new. For edits use `files_patch` with an
   `old_string` that spans the insertion point (safe to retry), then `files_read` to confirm.
5. **Back up before you overwrite.** `files_copy` the file to `domains/{domain}/_backups/…` or
   `files_archive` the folder before replacing working code. See [DEPLOY.md](references/DEPLOY.md).
6. **Give users the site URL with a trailing slash** — `https://{site}/apps/{name}/`. Identity
   (`X-Ucoz-User` / `ucoz_user()`) only arrives through `https://{site}/apps/…`, and a PHP folder URL
   without the slash is 301-redirected to the technical domain, where every visitor is a guest. The proxy
   strips `/apps` and sends `Host: {tech}`, so code routes on `/{name}/…` and builds public links from
   config, never from `HTTP_HOST`.
7. **Secrets never live in code or under `public_html`.** PHP: `domains/{domain}/{name}_secrets.php`.
   Node/Python: `config.json` in the app root (outside the docroot, never served) or `env_vars` at
   creation (they cannot be changed later through MCP). **`files_chmod` takes a decimal number:** 0600 =
   `"384"`, 0640 = `"416"` — `"600"` makes the file unreadable. See [SECURITY.md](references/SECURITY.md).
8. **Never echo secrets.** Database passwords (`db_create_with_user`), the visitor key
   (`visitor_key_get`), `env_vars` in `node_list`/`python_list`, `SetEnv` lines in generated
   `.htaccess`, cron command lines in `cron_list` (they carry keys), phpMyAdmin/SSO links, login keys,
   and the account e-mail in `user_config` stay out of
   chat, logs, comments, and reports. Write them straight into the secrets file; report only *where* they are.
9. **Shared-secret checks fail closed.** Cron, webhook, and admin endpoints use `require_key()` /
   `requireKey()` from the templates: a missing or short secret must never match an empty header (`hash_equals('', '')` is true —
   verified as an open endpoint). Webhooks also verify an HMAC signature with a timestamp.
10. **MySQL names get the `{username}_` prefix** (database and user). Save the password from the
   `db_create_with_user` response immediately — it is not shown again. MCP **cannot execute SQL**:
   schema is created by your code (idempotent migration). `db_create_user` alone grants **no** privileges —
   use `db_create_with_user`, or `db_change_privs` (**C**). `db_delete` leaves the user — delete it too.
   See [MYSQL.md](references/MYSQL.md).
11. **Every runtime gets an entry stub, because platform error output is invisible.** PHP: tiny
   `index.php` loads `lib/bootstrap.php` (handlers, own `app.log` and `php-error.log`), then `lib/app.php`.
   Node: `app.js` requires `lib/main.js` in `try/catch`. Python: `passenger_wsgi.py` imports `app.py` in
   `try/except`. Errors then come back as JSON 500 and are logged with file:line (PHP: `{name}_data/`;
   Node/Python: `logs/startup.log`) instead of a blank 500 or the Passenger "Web application could not be
   started" page (all three verified).
12. **Node/Python lifecycle.** Always pass `env_vars={"APP_NAME":"{name}"}` at `*_create` (the templates
   refuse to start without it; env vars cannot be changed later through MCP). Python: write
   `passenger_wsgi.py` **after** `python_create` (creation replaces it). A running app keeps old code until
   `*_restart` or a write to `tmp/restart.txt`. `public/` is not served as static; `console.log`/`print`
   output is lost — log to a file. Every Node response needs a `Content-Type` (npm install fails
   otherwise). Python packages: `python_install_modules` fails through MCP — run the venv `pip` from a
   one-off cron job ([PYTHON.md](references/PYTHON.md) §5). If a `*_create` call times out, check
   `*_list` before retrying — it may have succeeded. Never hand-edit the Passenger block in
   `public_html/<app_uri>/.htaccess`, never reuse an `app_uri` that is a PHP folder, never move `app_root`.
13. **`apps_write` writes UTF-8 text only.** Binary assets go to site storage via `files_read` / `files_write`, or the
   module downloads them itself (see recipe R4 — fonts).
14. **No shell, no SSH, no CGI, no background workers.** Code runs only when an HTTP request or a cron
   job hits it. Any request is cut off with a 504 at ~60 s (PHP stops at 30 s by default) — long work goes
   into cron batches. Test by requesting the live URL ([TESTING.md](references/TESTING.md)).
15. **Destructive and account-level actions need explicit approval of that exact action** and then
   `confirm: true` — see the gate table below.
16. **Public-link actions make data public.** `files_publish`, `backup_publish`, and large/binary
   `files_read` return a public URL (`https://{site}/apps/_mcp_dl/…`). Never use them on secrets or
   dumps unless the user asks; tell the user the link is public.
17. **Avoid huge outputs.** `php_get` returns ~100 KB. Call it only when changing PHP settings, and summarize.
18. **Verify on the live URL** after every deploy (status code, content type, body, one failure path)
   and report what you verified. "Saved" is not "working".
19. **Legacy uCoz PHP code is replaced, not edited.** Do not write or run code in the legacy wrapper
   (PHP 5.2, `ucoz_getinfo()`, `$___notjson`, responses like `({"SCRIPTDATA":"…","ERROR":""})`,
   OAuth 1.0a `uAPImodule.php`). When a site still depends on it, migrate the feature to Server
   Scripts: build next to it, switch one line of wiring, revoke the old credentials —
   [MIGRATION.md](references/MIGRATION.md). Also out of scope: anything uCoz already does natively
   (polls, comments, ratings, shop stock, mail forms) — use the built-in feature instead of a script.

## Confirmation gates

| Requires `confirm: true` after explicit approval | Ask first (no `confirm` flag, still consequential) |
|---|---|
| `activate`, `ensure_ready` (account creation) | overwriting an existing working file |
| `files_delete` (prefer `trash: true`), `files_move`, `files_extract`, `files_legacy_edit` | `php_set_version`, PHP option changes |
| `db_delete`, `db_delete_user`, `db_change_privs` | `db_import`, `db_change_hosts` |
| `node_destroy`, `python_destroy` | `node_stop` / `python_stop` on a live app |
| `cron_delete`, `ftp_delete` | `ftp_create`, `ftp_passwd` (creates credentials) |
| `backup_create` | `backup_publish`, `files_publish` (public link) |
| `visitor_key_rotate` / `visitor_key_delete`, `login_key_rotate` / `login_key_delete` | `visitor_key_create` (enables identity on all `/apps` requests) |

Never rotate the cached `main` login key unless the user asks.

## Delivery workflow

1. **Clarify** the job in one short block: what triggers it (page, form, webhook, schedule), who may
   call it (anyone, site members, admins, an external service), what data it stores, and
   where the result appears on the site.
2. **Design** the smallest thing that works: runtime, URL (`/apps/{name}/…`), files, storage
   (MySQL vs JSON file), auth model, secrets list. Show it to the user before writing when the
   change is non-trivial.
3. **Prepare** resources: database (`db_create_with_user`), secrets file, app (`node_create` /
   `python_create`), visitor key if identity is needed (with approval).
4. **Write** code from [templates/](templates/) adapted to the task; back up anything you replace.
   If you have a shell with the runtime, check it before deploying: `php -l`, a local Node/Python run,
   the logic on a small real data sample ([TESTING.md](references/TESTING.md) §1).
5. **Verify** the deployed module on its own URL with the smoke checklist in [TESTING.md](references/TESTING.md):
   health (with the new `BUILD` marker for Node/Python), happy path, one failure path (bad input → 4xx
   JSON, not 500), private files not served. Visitors do not see it yet.
6. **Wire** into the site only after that, with approval: a template snippet or page that calls
   `/apps/{name}/…` (same origin — no CORS needed). For visible changes, show a demo page first. Use
   `templates_read` / `templates_write`; follow the design skill if one is installed. Then check the real site page.
7. **Report** (template below) and run Rule 0.

## Reference router

| Task | Open |
|---|---|
| What the environment is, URL mapping, limits, versions | [ENVIRONMENT.md](references/ENVIRONMENT.md) |
| Every `apps_read` / `apps_write` / `apps_delete` action: params, gates, response shapes, quirks | [APPS_TOOL.md](references/APPS_TOOL.md) |
| PHP module pattern, error handling, limits, uploads | [PHP.md](references/PHP.md) |
| Node.js app under Passenger | [NODE.md](references/NODE.md) |
| Python app under Passenger | [PYTHON.md](references/PYTHON.md) |
| MySQL: create, connect, migrate, avoid races | [MYSQL.md](references/MYSQL.md) |
| Scheduled jobs | [CRON.md](references/CRON.md) |
| "Who is logged in" — `ucoz_user()` and the visitor key | [IDENTITY.md](references/IDENTITY.md) |
| Calling the site uAPI from server code | [UAPI.md](references/UAPI.md) |
| Secrets, auth, CSRF, public links, logging hygiene | [SECURITY.md](references/SECURITY.md) |
| Deploy, back up, roll back | [DEPLOY.md](references/DEPLOY.md) |
| Test before deploy, after deploy, before and after switching the site: local checks, smoke checklist, restart proof, cron, identity, demo page | [TESTING.md](references/TESTING.md) |
| Design an app: runtime choice, module files, request flow, JSON vs SQLite vs MySQL, uAPI mirror, cron jobs | [ARCHITECTURE.md](references/ARCHITECTURE.md) |
| Something is broken | [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md) |
| Replace an old script on the legacy uCoz PHP server (`$___notjson`, `/php/…`, OAuth 1.0a uAPI) | [MIGRATION.md](references/MIGRATION.md) |
| The user wants something the panel cannot do (bot, dashboard, top materials, search, digest, integration) | [CAPABILITIES.md](references/CAPABILITIES.md) |
| End-to-end recipes (each with its own test status): lead form → MySQL (+ Telegram/CRM notify), Node members API, signed webhooks → queue → cron, cached external-data widget, OG images, Telegram bot via polling, fixing a broken module, migration | [examples/RECIPES.md](examples/RECIPES.md) |
| Starter code | [templates/](templates/) — `php/` (stub `index.php`, `lib/bootstrap.php`, `lib/app.php`, `.htaccess`), `node/` (stub `app.js`, `lib/main.js`), `python/` (stub `passenger_wsgi.py`, `app.py`), `probe/` |

## Final report template

```text
Deployed: <what it does, one line>
URL(s):   https://{site}/apps/<...>   (verified: <status code / what was checked>)
Runtime:  PHP <ver> | Node <ver> app_uri=/<x> | Python <ver> app_uri=/<x>
Files:    domains/{domain}/...  (backups: <paths>)
Data:     MySQL {username}_<db> (tables: ...) | JSON file <path> | none
Secrets:  stored in <path> — values not shown
Cron:     <schedule> → <command without secrets> | none
Wiring:   <template / page changed, or "not wired yet">
Next:     <anything the user must do: e.g. paste uAPI key, enable visitor key>
```

Then Rule 0.
