| Tool | What it does | |
list_sites | List the customer's hosted sites with their state, URL, live health and deploy state. Start here — other site tools take the returned slug. | reads |
get_site_health | Live health for one site: probe result with freshness, current state, deploy state, and any open incidents affecting it. Works on suspended or provisioning sites too, so it can diagnose a site that is down. | reads |
get_site_logs | Recent log lines from a site (last 200, oldest first), plus `entries`: the same lines folded into events, each with its UTC `time` (null when unreadable), `level`, `message`, `exception`, the `release` it ran from and `after_live_deploy` (compared with `live_deploy_at`). A multi-line Laravel error (stack frames, "[previous exception]" lines) is ONE entry: never read a continuation line as a failure of its own, and judge whether an error is current from its time and after_live_deploy, not from the line. Choose the log with `log`: app (application errors), php (php-fpm), nginx-error (web server errors), access (visits). The first place to look when a site misbehaves. | reads |
list_deploys | Deploy history for a site (last 20): source, git ref, commit, status and time. Use after a deploy to confirm it finished. | reads |
list_backups | Restore points for a site (last 20) with id, filename, kind and size. | reads |
get_site_traffic | Disk usage and network traffic for a site. period_* fields are the current billing month; net_* are since boot. Values are null until the host sampler has run. | reads |
get_site_analytics | Request-level analytics for a site, derived from its access log. Returns 24h/7d/30d rollups (requests, unique visitors, bot vs human, response-code breakdown, top pages/referrers/browsers) plus hourly and daily time-series. Errors mean server errors: err_rate is 5xx / requests. 4xx responses (not found, refused) are mostly scanners and spam bots being turned away and are NOT errors; they are reported separately (rate_4xx, s4_bot/s4_human, codes_4xx, top_4xx_paths, top_4xx_sources). An analytics file written before October 2026 may still carry the old blended err_rate, so compute the server-error rate from status.s5 / req. Values are empty until the host sampler has run. | reads |
get_staging_status | Staging copy status for a site: enabled, state, URL and last sync time. (Creating or refreshing staging is a write action — do it in the portal for now.) | reads |
list_cron_jobs | Scheduled tasks (cron jobs) configured on a PHP-runtime site, with their index, schedule and command. | reads |
list_domains | Domains registered on the customer's account: status, registration/expiry dates, auto-renew, transfer lock, DNSSEC and transfer state. | reads |
list_mail | Email on the account: every domain with veldhost email (email-only domains and sites with email on), how many mailboxes each allows and uses, and every mailbox with its status — active, locked (payment overdue: still receives, nobody can sign in) or archived (removed, mail kept until deleted_for_good_at). Mailboxes are created in the portal, never here: that takes a password. Requires the read scope. | reads |
get_mail_domain | One email-only domain and what it still needs. Unverified: the TXT record that proves it is the customer's. Verified: whether its mail reaches veldhost yet and, when its DNS is hosted elsewhere, the exact MX/SPF/DKIM/DMARC records to add there (records_to_add). Requires the read scope. | reads |
get_domain_registrar | Registrar-level state for one domain: the nameservers on file at the registry versus what public resolvers answer, whether it is really on veldhost DNS, the DNSSEC state (signed here, DS at the registry, or mismatched), the transfer lock and WHOIS privacy. To CHANGE any of it, send the user to https://manage.veldhost.eu/portal/domains. | reads |
list_dns_zones | DNS zones the account manages on veldhost's nameservers, with DNSSEC signing state and whether the domain is registered here. Zones also exist for connected (externally registered) domains. | reads |
list_dns_records | All customer-editable DNS records in a zone, plus the veldhost nameservers. System records (SOA, apex NS, DNSSEC) are managed by the platform and hidden. | reads |
search_domains | Check domain availability and yearly price for a keyword (fans out over common TLDs) or a full domain. Read-only: it NEVER registers or charges. veldhost does not sell domain registrations self-serve — when the response carries offered=false, the supported path is to bring a domain you already own (point its nameservers at veldhost) or to email support for a registration in your own name; follow continue_url. total_price covers TLDs with a multi-year minimum (e.g. .gr, .hu). | reads |
quote_new_site | Price a new site and get a portal continuation URL. This NEVER creates anything and NEVER charges: creating a site starts a paid subscription, which requires the customer's own consent and checkout in the portal. Give the returned continue_url to the user — the form arrives pre-filled and they finish in one click. | reads |
get_site_settings | A site's settings: PHP or Node.js version (and the versions available), start command, build steps, connected domain and its state, and environment variable names. Secret values are never shown. | reads |
list_team | Who is on the account and their role (owner, admin, developer, billing, viewer), and invites not yet accepted. Read-only: an owner or admin changes the team in the portal. Useful to tell the user who can approve an order (owner or billing). | reads |
get_domain | One registered domain: expiry, auto-renew, transfer lock, whois privacy and nameserver mode. For the deeper question of whether the registry and public resolvers actually agree, use get_domain_registrar instead. Requires the read scope. | reads |
list_invoices | The account's invoices, newest first: number, period, amount, currency, status and when each was paid. Answers "what am I paying for" and "was last month settled" without a trip to the portal. Read-only — nothing here can charge, refund or change a plan. Requires the read scope. | reads |
get_audit_log | The account's audit trail, newest first: who did what and when, across the portal, the API and this MCP server. The tool to reach for after something changed unexpectedly — "what happened before the site went down", "who edited that DNS record". Requires the read scope. | reads |
export_dns_zone | The whole zone as a standard BIND zone file, in one call. Far cheaper than paging list_dns_records when you need the full picture — to review a zone, diff it against another provider, or keep a copy before a bulk change. Pairs with import_dns_zone. Requires the read scope. | reads |
| Tool | What it does | |
set_env_var | Create or replace one runtime environment variable on a site (UPPER_SNAKE_CASE). Applies to the running app within a minute. Mark API keys and passwords secret=true so their value is never shown again. Variables set by veldhost (database, app key) cannot be changed. | changes something |
delete_env_var | Remove one runtime environment variable from a site. The app restarts without it within a minute; code that reads it may break. Confirm with the user first. | changes something |
set_runtime | Switch a site's PHP version, Node.js version, or Node.js start command (get_site_settings lists the versions on offer). The app restarts on it within a minute; check get_site_health afterwards. | changes something |
connect_domain | Connect a custom domain the customer owns to a site. Answers connected (with what the customer does next: set our nameservers at their registrar, or keep their DNS and point @ and www at the given address), or needs_portal with a continue_url when the domain has email that must be copied across first. The certificate is issued automatically once the domain reaches us. Registering a new domain is done in the portal. | changes something |
list_webhooks | The webhooks and chat channels (Slack, Teams) this login has set up: where each posts, which events it sends, and whether it is enabled. Secrets are never shown. | reads |
create_webhook | Send veldhost events to an https URL: a signed JSON webhook (kind generic), or a Slack or Microsoft Teams incoming-webhook URL. Events: deploy.succeeded, deploy.failed, backup.completed, restore.completed, site.down, site.up, incident.opened, incident.resolved, domain.expiring, invoice.paid, invoice.payment_failed, job.succeeded, job.failed, job.report. A generic webhook's signing secret is in the answer ONCE: tell the user to store it now, it cannot be shown again. | changes something |
delete_webhook | Remove a webhook or chat channel (id from list_webhooks). Its deliveries stop at once. | changes something |
test_webhook | Send a test delivery to a webhook and report what it answered, to check the URL and signature handling. | changes something |
run_backup | Create a backup of a site right now. Queues the backup; see list_backups for the result. Requires the manage scope. | changes something |
restore_backup | Prepare restoring a backup OVER the live site (current content is replaced). This tool never restores by itself: it checks the backup and returns a confirm_url in the veldhost portal where the user clicks Restore. Give the user that link. Requires the manage scope. | changes something |
create_staging | Create a password-protected staging copy of a site (owner-only). On the Starter plan this bills the staging add-on; Pro and Business include staging. Takes a couple of minutes — poll get_staging_status. Requires the manage scope. | changes something |
refresh_staging | Re-copy the LIVE site over its staging copy (owner-only). DESTRUCTIVE for staging: any changes made on staging are replaced. Requires the manage scope. | changes something |
disable_staging | Turn off and destroy a site's staging copy (owner-only). Billing for the staging add-on stops. Requires the manage scope. | changes something |
add_cron_job | Add a scheduled task to a PHP-runtime site: 5-field cron `schedule` plus a `command` starting with php or wp (no shell operators, max 5 jobs). Requires the manage scope. | changes something |
remove_cron_job | Remove a scheduled task from a site by its index (from list_cron_jobs). Requires the manage scope. | changes something |
| Tool | What it does | |
list_databases | List the customer's databases: site databases (MariaDB and PostgreSQL, role "app", read-only) and warehouse databases (MariaDB, role "warehouse", writable with run_warehouse_statement), with engine, state, whether the plan includes querying (plan_ok) and the site or warehouse they belong to. Start here for any data question — get_schema, run_query and run_warehouse_statement take the returned id. Needs the opt-in `data` scope. | reads |
get_schema | Tables, views and columns (name, type, nullable, key, default) of one database, with any notes the customer wrote. The first call prepares the database and answers state "preparing" — wait a few seconds and call again. Use the exact table names it returns in run_query. Needs the opt-in `data` scope. | reads |
run_query | Run ONE read-only SQL statement (SELECT, WITH, SHOW, EXPLAIN, DESCRIBE, VALUES) against a site or warehouse database and return columns + rows. Always runs read-only, as a read-only login: writes, DDL, locks, several statements and sleep/file functions are refused — to change a warehouse database use run_warehouse_statement. Rows are capped by limit (max 1,000) and 30 seconds; truncated=true means there was more. Use MariaDB or PostgreSQL syntax to match the database's engine from list_databases. Needs the opt-in `data` scope. | changes something |
run_warehouse_statement | Run ONE statement that changes a WAREHOUSE database (database with role "warehouse" from list_databases): INSERT, UPDATE, DELETE, REPLACE, CREATE/ALTER/DROP TABLE, VIEW or INDEX, TRUNCATE, RENAME TABLE — or a SELECT. MariaDB syntax. Grants, files, procedures, transactions, several statements and system objects are refused. A destructive statement (DROP, TRUNCATE, DELETE or UPDATE without WHERE, ALTER … DROP, RENAME) is not run: the answer is a summary of what it would destroy plus a confirm_token. Relay the summary to the user, and only once they agree call again with that confirm_token (same database_id and sql). Returns statement_kind (read|write|ddl), affected_rows, last_insert_id, and rows for a SELECT. Site databases are read-only and refuse this tool. Needs the opt-in `data` scope. | changes something |
create_warehouse_database | Create a new database in the customer's warehouse (a database container every plan includes — Starter 2 databases and 5 GB, Pro 10 and 25 GB, Business 20 and 75 GB; the first database also creates the warehouse, which takes about a minute). The name is 3–32 lowercase letters, digits and underscores. Answers 202 with the pending database — it appears in list_databases with state "active" once created. The database ceiling is the account's warehouse tier (list_databases reports it). Needs the opt-in `data` scope; a token limited to particular sites cannot do this. | changes something |
list_dashboards | List the customer's dashboards (title, description, tags, panel count, starred, portal url, uid). get_dashboard, add_panel and render_panel_data take the returned uid. Needs the opt-in `data` scope. | reads |
get_dashboard | One dashboard's full model: time range, refresh, variables and every panel (key, type, title, database_id, grid, options, and the SQL when the token may query that database — sql_hidden otherwise). Needs the opt-in `data` scope. | reads |
create_dashboard | Create a dashboard on the customer's account (owner, admin or developer, on an account with an active plan). Optionally with panels right away — each panel needs type (timeseries | bar | stat | gauge | table | piechart), title, database_id (from list_databases) and sql. Panel SQL is read-only and may use the dashboard macros, expanded on the server for the current time range: $__timeFilter(col) → a `col >= from AND col < to` condition, $__timeFrom() / $__timeTo(), $__unixEpochFilter(col), $__unixEpochFrom() / $__unixEpochTo(), $__interval (an INTERVAL of the automatic bucket width), $__interval_s, $__range_s, $__timeGroup(col[, interval]) (col bucketed to the width) and $__timeGroupAlias(col[, interval]) (the same AS time), plus ${name} for a dashboard variable (always inserted as a quoted string literal; multi-select gives 'a','b'). A time series panel expects a first column named time and numeric columns after it: SELECT $__timeGroupAlias(created_at, $__interval), count(*) AS events FROM events WHERE $__timeFilter(created_at) GROUP BY 1 ORDER BY 1. Dashboards are UTC. Needs the opt-in `data` scope. | changes something |
add_panel | Add one panel to an existing dashboard, below the others unless a grid position is given. The SQL runs read-only with the caller's right to query the database and may use the dashboard macros ($__timeFilter(col), $__timeGroupAlias(col, $__interval), $__interval, ${variable}, … — see create_dashboard). Needs the opt-in `data` scope. | changes something |
render_panel_data | The rows behind one dashboard panel for a time range (default: the dashboard's own): columns + rows, whether they came from the cache, what the time range and interval resolved to, and the resolved SQL when the token may query that database. Waits up to wait_s for a fresh run; a 202 answer means it is still running — call again. Variables are passed as {name: value} or {name: [values]}; "$__all" selects every option. Needs the opt-in `data` scope. | reads |
list_jobs | List the customer's jobs — scheduled work on their own data. Each one shows its name, trigger (a cron schedule in their time zone, after another job, or manual), whether it is switched on, when it next runs, its steps, and how the last run went. run_job and get_job_runs take the returned id. Needs the opt-in `data` scope. | reads |
create_job | Create a job on the customer's account (owner, admin or developer, on an account with an active plan): a set of steps we run for them, one at a time.
Order comes from `depends_on` on each step: a list of {"key": "<another step>", "when": "success|failure|always|true|false"} ("success" if you omit `when`). A step with no depends_on is a root. Send a plain list with NO depends_on anywhere and it runs top to bottom, stopping at the first failure unless that step says continue_on_failure. Steps run one at a time in dependency order — branches are how you say "only if", not "at the same time". Add "pos": {"x": N, "y": N} to place a step on the customer's canvas; it does not affect what runs.
Step types, and the keys each one needs:
sql {database_id, sql} — up to 50 statements against a WAREHOUSE database they may write (never a site's own database); from list_databases, role "warehouse".
refresh_dashboard {dashboard_uid, time?: {from, to}} — warms every panel so the dashboard opens with numbers.
notify_email {to: {members: [user ids on the account]}, subject, body, only_on?: always|success|failure, attach?: {database_id, sql}} — recipients are account members only; the body may use {{job.name}}, {{run.status}}, {{run.duration}}, {{run.url}} and {{step.<key>.rows_written|rows_read|status}}; attach runs a read-only query and sends it as CSV.
notify_webhook {endpoint_id, only_on?} — one of the account's own webhook endpoints.
wait {seconds} — 1 to 300.
run_job {job_id, wait?} — another job on the account.
run_pipeline {pipeline_id, full_refresh?} — builds one of the account's pipelines (from list_pipelines). full_refresh rebuilds every table it writes instead of topping them up.
condition {name, test} — ask a question and branch on the answer. test is either {"kind":"output","step":"<earlier step>","field":"rows_written|rows_read|status|warmed|failed|affected_rows|statements","op":"==|!=|>|>=|<|<=|is_null|is_not_null","value":N} or {"kind":"sql","database_id":N,"sql":"SELECT …","op":…,"value":…} (one read-only SELECT; its first cell is compared). The step itself always succeeds — the steps that depend on it with when "true" or "false" are what branch. Only a condition can be depended on for true/false.
Triggers: {"kind":"manual"}, {"kind":"schedule","cron":"0 3 * * *","timezone":"Europe/Riga"} (five fields, read in the customer's own zone so "03:00" survives a DST change; the minimum interval is 5 minutes on Pro, 1 on Business), or {"kind":"after_job","job_id":N}.
Never guess a database or dashboard: take the ids from list_databases and list_dashboards. Steps that wait for each other in a loop are refused (error type `cycle`). Needs the opt-in `data` scope. | changes something |
run_job | Run one of the customer's jobs now (owner, admin or developer). Answers with the run to follow — poll get_job_runs, or read the run's id back. One run per account at a time: a job that is already running answers with a run whose status is "skipped" (concurrency skip) or "queued" (concurrency queue), which is the platform declining on purpose, not a failure. When the account's monthly spend cap on job minutes is reached the call is refused with error type "usage_cap" and a billing_url where an owner can raise the cap; a scheduled run in that state is recorded "skipped" with skip_reason "cap". Needs the opt-in `data` scope. | changes something |
get_job_runs | The run history of one job, newest first: when each run started and finished, how long it took, how many rows it read and wrote, which step failed if one did, and the outcome of every step. Use it to answer "did last night's job work" and "why did it stop". Needs the opt-in `data` scope. | reads |
list_pipelines | List the customer's pipelines — the SQL that builds tables in their warehouse. Each one shows its name, the warehouse it writes into, its SQL files, the tables it builds, the dependency graph we derived from the SQL, whether it currently compiles, and how the last run went. run_pipeline, preview_pipeline and get_job_runs take the returned id. Needs the opt-in `data` scope. | reads |
create_pipeline | Create a pipeline on the customer's account (owner, admin or developer, on an account with an active plan): a folder of SQL files that build tables in one of their warehouse databases, on a schedule when a job runs it.
A pipeline is its SQL: up to 20 files, each up to 64 KB, named like "orders_by_day.sql". The databases are NAMED IN THE SQL by the `name` list_databases shows — `shop.orders`, or `shop.public.orders` on PostgreSQL — so there is nothing to wire up: `target_database_id` and `sources` are optional and only matter for a pipeline written before names were enough.
Each file holds any number of `;`-separated statements, and there are exactly two forms. `--` and `/* */` comments are fine.
CREATE OR REFRESH TABLE [<warehouse>.]<name> [KEY (col, col)] AS <select>
Materialised in the warehouse, in that warehouse's own engine (MariaDB or PostgreSQL). Write `<warehouse>.<name>` to say which warehouse; with one warehouse on the account the name alone will do. With KEY the rows are upserted on those columns, so a nightly run only rewrites what changed; without KEY the table is rebuilt every run. The SELECT may read source views and other tables of this pipeline by name, tables of the warehouse by name, and ANY table of a site database as `<database>.<table>` — that table is then pulled whole into the warehouse for the run and joined there, which is how one statement reads two databases.
CREATE OR REFRESH SOURCE VIEW <name> [FROM <database>] AS <select>
Runs on the site database, in ITS OWN dialect (MariaDB or PostgreSQL — check `engine` in list_databases), through a read-only login, and is streamed into the warehouse as a staging table. Use it to pull only the rows you need: its WHERE runs on the site. `FROM <database>` names the database; leave it out and the qualified names in the SELECT say which one. It may NOT read anything the pipeline defines.
Names are 1-64 lower-case letters, digits and underscores and are unique across the whole pipeline. The order tables are built in is DERIVED from the SQL: every identifier after FROM or JOIN that matches a name the pipeline defines is a dependency, so write `FROM other_table` rather than a comma join. A loop is refused, and so is building in two warehouses.
Example:
files: [{"name": "analytics.sql", "sql":
"CREATE OR REFRESH SOURCE VIEW recent_events FROM ecotrend AS\nSELECT id, created_at, path FROM analytics_events WHERE created_at > now() - interval '400 days';\n\nCREATE OR REFRESH TABLE dw.analytics_by_day KEY (day, path) AS\nSELECT DATE(created_at) AS day, path, COUNT(*) AS events FROM recent_events GROUP BY 1, 2;"}]
Call compile (via preview_pipeline, or by reading `compile` in this tool's answer) before telling the user it is done: every problem comes back with the file and the line. Never guess a database or a table name — take them from list_databases and get_schema. Needs the opt-in `data` scope. | changes something |
preview_pipeline | Try one of the customer's pipelines on a small sample without writing anything: each source view is staged with a couple of hundred rows, each table is built into a temporary table, a sample of the rows comes back, and everything is dropped again. Nothing the customer already has is changed. Use it to check that the SQL does what you meant before run_pipeline. `files` limits it to the statements in those files (plus the source views they need). Waits up to wait_s seconds (30 max) for the answer; if it is still going, poll the run with get_job_runs. Needs the opt-in `data` scope. | reads |
run_pipeline | Build one of the customer's pipelines now: stage every source view and build every table, in the order the SQL implies (owner, admin or developer). Answers with the run to follow — poll it with get_job_runs, or read the run id back. One run per account at a time: a pipeline asked for while something else is running answers with a run whose status is "skipped", which is the platform declining on purpose, not a failure. `full_refresh` rebuilds even the tables that would normally be upserted. Needs the opt-in `data` scope. | changes something |
cancel_job_run | Stop a job run that is queued or still running. Safe and recoverable: the run is marked cancelled and can be started again with run_job. A run that has already finished is left alone. Use it when a job is stuck, looping, or was started by mistake. Needs the opt-in `data` scope. | changes something |
get_job_run | One job run in full: its state, timings, rows read and written, and the outcome of every step. Poll this after run_job to follow a run to completion rather than listing every run of the job. Needs the opt-in `data` scope. | reads |
list_job_runs | Recent job runs ACROSS every job, newest first — the answer to "what is running right now" and "what failed overnight". get_job_runs covers one job; this one spans the account. Needs the opt-in `data` scope. | reads |
get_job | One job in full: its schedule, steps, target database and current state. Read this before editing or re-running a job, instead of pulling the whole list to find one row. Needs the opt-in `data` scope. | reads |
get_pipeline | One pipeline in full: its stages, sources, destinations and schedule. Read this before preview_pipeline or run_pipeline so a change is made against what is actually configured. Needs the opt-in `data` scope. | reads |
list_pipeline_runs | The run history of one pipeline, newest first: when each run went, how long it took, and which stage failed if one did. The pipeline equivalent of get_job_runs. Needs the opt-in `data` scope. | reads |
get_data_run | The state and result of one query run started by run_query. Use it when a query did not return inline — a long query keeps going after the call returns, and this is how to collect it. Needs the opt-in `data` scope. | reads |
update_job | Change a job: its schedule, steps, budget or whether it is switched on. This REPLACES the definition, so read the job with get_job first and send the whole thing back with your change applied — fields you leave out are not preserved. Needs the opt-in `data` scope. | changes something |
delete_job | Delete a job and its schedule. The job stops running; its run history goes with it. Use cancel_job_run instead if you only want to stop the run that is happening now. Needs the opt-in `data` scope. | changes something |
update_pipeline | Change a pipeline's SQL files, target or description. This REPLACES the definition, so read it with get_pipeline first and send the whole thing back with your change applied. Run compile_pipeline afterwards to check the SQL still builds before anyone waits on a scheduled run to find out. Needs the opt-in `data` scope. | changes something |
delete_pipeline | Delete a pipeline and its schedule. Its run history goes with it. The data it has already written to its destinations is NOT removed. Needs the opt-in `data` scope. | changes something |
compile_pipeline | Check that a pipeline's SQL parses and its models resolve into a valid build order, without running anything or touching any data. The cheap check to make after update_pipeline, instead of discovering a broken pipeline when its schedule fires. Needs the opt-in `data` scope. | reads |
update_dashboard | Change a dashboard's title, tags, default time range, refresh rate, panels or variables. This REPLACES the dashboard, so read it with get_dashboard first and send the whole thing back with your change applied. To change one panel, prefer update_panel. Needs the opt-in `data` scope. | changes something |
delete_dashboard | Delete a dashboard and all of its panels. Only the dashboard is removed — the underlying data is untouched, so it can be rebuilt. Needs the opt-in `data` scope. | changes something |
update_panel | Change one panel on a dashboard — its SQL, type, title, position or display options — leaving the rest of the dashboard alone. Prefer this over update_dashboard when only a single panel is changing. Needs the opt-in `data` scope. | changes something |
delete_panel | Remove one panel from a dashboard. The rest of the dashboard is left as it is, and the panel can be added back with add_panel. Needs the opt-in `data` scope. | changes something |
delete_warehouse_database | Permanently drop a warehouse database and everything in it. This destroys DATA, not just a definition, and it cannot be undone. Two-step: the first call drops nothing and returns a warning with a confirm_token; relay it, and only after the user agrees call again with that confirm_token. Needs the opt-in `data` scope. | changes something |