Lathe
Managed Postgres 17, Redis 8, CouchDB 3 and NATS 2 on a dedicated EU machine, plus your app and its sign-in - one flat monthly price. This MCP server lets an agent provision machines, pull connection strings, create databases and manage streams. OAuth 2.1 sign-in or API key.
Hosted MCP Server
npx add-mcp 'https://mcp.lathe.live/mcp'Installs into Claude Code, Codex, Cursor and more
Documentation
API and MCP
Everything the portal does, from code or from an AI agent. Same rules, same checks, same jobs.
API keys
One credential for both the REST API and the MCP server.
Sign in, then create a key on the Account page. A key acts as the customer who created it and cannot create or revoke other keys. Send it on every request:
Authorization: Bearer lathe_…
MCP server
Streamable HTTP · https://mcp.lathe.live
Claude.ai and other clients that sign you in
Settings → Connectors → Add custom connector: name it Lathe, paste the URL above, keep Requires sign-in on and leave the client id and secret blank (the server registers the client itself). Claude sends you here to sign in with your email link and approve access; the app then appears under Connected apps, where you can disconnect it.
https://mcp.lathe.live
Clients that take a header
Give the agent the endpoint and an API key. Claude Code (which can also sign in: add it without the header, then run /mcp):
claude mcp add --transport http lathe https://mcp.lathe.live --header "Authorization: Bearer lathe_…"
Claude Desktop, Cursor and most other clients take a JSON entry of this shape:
{
"mcpServers": {
"lathe": {
"type": "http",
"url": "https://mcp.lathe.live",
"headers": { "Authorization": "Bearer lathe_…" }
}
}
}
Tools
| Tool | What it does |
|---|---|
list_plans read | Plans with prices (USD per month), hardware, price per GB, and the locations an instance can be created in. |
get_account read | The account behind this key: email, profile (name, company, tax id, address), card on file, and whether another instance can be created now (new accounts run one instance until their first renewal). |
update_account | Update the profile that goes on receipts. Omitted fields keep their value; the name is required before the first purchase. |
get_billing read | Subscriptions (status, tier, next charge, trial_ends_at while a trial has not been charged yet) and the charge history. |
list_instances read | The customer's instances with status, tier, location, tags, billing status and any job in progress. No secrets: use get_connection_urls for those. |
get_instance read | One instance in full: status (awaiting_payment / pending / creating / running / …), billing, disk and connection usage, 24h monitoring (load, memory, disk, database size, TPS, cache hit, long queries), backups, recent activity, upgrade options and the pending job if any. |
get_connection_urls read | Connection strings for an instance: direct (5432) and pooled (6432, transaction pooling), and one entry per other engine under `engines`. The credentials are TEMPORARY: each entry's `access` block says when it stops working (an hour unless ttl_minutes says otherwise, 5 to 720; the auth engine's key at most 60) and names the lease_id that refresh_access extends with the person's approval. A permanent credential is not this server's to give: the portal's instance page and the REST API's connection endpoint have it, and an app hosted on the box already has every engine's URL in its environment. `engines` names which engines to lease (default: all on the box). The auth engine's entry carries its URL, which is a name of its own (or the customer's, when auth_domain is set) with /auth/v1 under it rather than the box's own name, the anon key a browser may hold, a service key that opens its admin API until it expires, the JWKS URL and the loopback address apps on the box use. Handle as secrets. |
refresh_access | Extend a temporary credential (a lease_id from get_connection_urls) by ttl_minutes from now (default 60). A person must approve: the first call answers status approval_required and an approval_url - ask the person to open it and click Extend, then call again within 10 minutes. The next call answers status extended: for Postgres, Redis, CouchDB and NATS the credential you hold keeps working until the new expires_at; for the auth engine `entry` carries a new key, since a key's expiry cannot be moved. A lease that has ended cannot be extended: call get_connection_urls again. |
revoke_access | End a temporary credential now (its connections are closed). The auth engine's key cannot be ended early and lapses at its expires_at; the answer says so. |
list_access read | The live temporary credentials on this account (or on one instance): engine, principal, expiry, and the API key or connected app that minted each. No secrets. |
create_instance | Create an instance on a plan (mini | starter | plus | pro). This costs money: a paid plan returns checkout_url, which the person must open in a browser and pay - the instance is created when the payment page returns, and the returned instance_id is valid from the start (poll get_instance). A coupon that covers the whole first month creates it immediately. Where list_plans reports a trial, a customer's first Mini answers trial=true: with trial.card 'later' it is created at once with no checkout (a card added on the portal keeps it past the trial); with 'start' checkout_url verifies the card and nothing is charged until the trial ends. Optional: name (a label), tags (lowercase tokens), location (from list_plans), coupon, engines (names from list_plans; default Postgres alone). |
update_instance | Rename or retag an instance. Omitted fields keep their value; an empty name or an empty tag list clears it. Allowed while a job is running. |
configure_engines | Set which engines run on the box and their memory budgets in MB: {name: {memory_mb}} for every engine wanted (omit memory_mb to split what is left). The plan's budget and each engine's floor are in list_plans. A budget alone moves without a restart (Postgres takes its new shared_buffers at its next restart). An engine left out is stopped with its data kept on disk; at least one engine stays on. The auth engine (Auth: sign-in for the customer's app, its users in the instance's Postgres) runs behind the apps engine, so it needs postgres and apps on too; its keys and URL come from get_connection_urls, its site URL, mail and OAuth providers from configure_engine_settings, and an app that declares links: ["auth"] gets AUTH_URL and the keys in its environment. |
configure_reallocation | Automatic reallocation on or off for an instance: on (the default), memory moves between its engines and apps from what each one holds, inside the plan's budget, at most once a day and with nothing restarted; off, the advice is kept on the instance (get_instance, under reallocation.advice) for reallocate_now. Allowed while a job runs. |
reallocate_now | Move memory between the instance's engines now, as the advisor would from the last day of readings; nothing restarts. Refused with the reason when there is nothing to move, or when the box has no room to give and the answer is a bigger plan (upgrade_instance - never taken on its own). |
set_allowlist | Replace the IP allowlist for ports 5432 and 6432 with a list of CIDRs. An empty list opens the instance to the internet (TLS and password still required). Takes effect within a minute. |
configure_engine_settings | REPLACE one engine's settings: a key you leave out goes back to its default, so send the whole set or call update_engine_settings instead, which is what you want for one or two keys. Keys, bounds and defaults are in list_plans under engines[name].settings, each with the `group` it belongs to. Postgres: statement_timeout_ms, timezone. Redis: maxmemory_policy. NATS: max_payload_mb. The auth engine has about a hundred, grouped: general (site_url, redirect_urls, signups, email_signin, magic_link, authorized_addresses, otp_expiry_s, otp_length, admin_api, auth_domain), providers (an OAuth <name>_client_id and <name>_secret for twenty of them, github and google among them, plus a <name>_url for azure, gitlab, keycloak and workos), mail (smtp_*, emails_per_hour, mail_min_interval_s, a template and a subject per kind, the notification mails), security (password_min_length, password_characters, password_breach_check, captcha, refresh token rotation and reuse, manual_linking, the per-address rate limits), sessions (jwt_expiry_s, session_timebox_h, session_inactivity_h, single_session) and mfa (totp, passkeys, webauthn, recovery_codes, max_factors). An on/off setting takes on or off. A secret reads back masked; the mask sent back keeps it. The engine restarts or reloads. |
update_engine_settings | Change some of one engine's settings and leave every other key as it is - the safe way to set one thing, since configure_engine_settings resets what it does not carry. Same keys as that tool. An empty value puts a key back to its default, which is how a provider or a custom domain is cleared. The engine restarts or reloads. |
app_guide read | How to run the person's own app on a box, as markdown, for this deployment: the container image contract (listen on PORT, answer the health path, keep state under /data), every field set_app takes, what the box injects into the environment, how a deploy moves traffic, and a GitHub Actions workflow that builds and deploys on every push (or, where the person can connect their repository with connect_app_repo, a workflow that only builds and pushes, since the green run is then what deploys). Read it before writing a Dockerfile, a deploy step or a CI workflow for a box, and before the first set_app: it is the contract those calls are checked against. |
list_apps read | The apps running on the box: image, port, health path, whether the front door serves them and at which URL (a public app's own name; the one marked default answers on the box's hostname too), custom domains, memory, when each was last deployed, and the names (never the values) of its environment variables - `env_keys` the customer's own, `injected` what the box sets for it keyed by what contributes (PORT, each engine on the machine, each app it links), and `injected_missing` an engine whose keys this app is not given because it does not link it, so an empty `env_keys` means you set none rather than that the app has no environment. `memory` carries what the app is actually holding against its ceiling and says so when it has been held back at it: `peak_mb` is the kernel's own high-water mark where it has one (`lifetime_peak_mb`), which a container that spikes between two five-minute readings is far above `sampled_peak_mb`. `secrets_refreshed_at` is when the machine last fetched the box's credentials, to compare with an app's `deployed_at`: `injected` says which names an app is given, and those two together say whether the container running now started with them. `pending` lists apps imported from a repository that are still waiting for their first build. |
set_app | Add an app to the box, or change one by name. Every field but the name is optional: one you do not pass keeps the value the app already has (and takes the default below on an app being created), so this call never moves a setting you did not name. An app being created does need an image; one that exists keeps the image it runs. To change one thing on an app that exists, update_app says so plainly. The box pulls the image, starts it beside the version already running and moves the front door to it once it answers health_path; a version that never answers is left out and the running one stays. public=true serves it over https on a name of its own, <instance>-<name>.<the box's zone>, and on any domains (which must point at the box first); default=true makes it the app the box's own hostname goes to as well, one per box (the first public app, when none is marked). public=false is loopback only, for the box's other apps. An app owns its port and the two above it, so leave three between apps. memory_mb comes out of the apps engine's budget (configure_engines sets that). singleton=true for an app that must never run twice: it is restarted in place, so a deploy has a moment of downtime. links names the other apps this one talks to (each becomes <NAME>_URL in its environment); nothing else about a neighbour reaches it. read_only runs it on a read-only root once the image tolerates one; capabilities names the Linux capabilities it needs, none by default. This runs the image you name on the box: name one you trust. |
update_app | Change some of one app's settings and leave every other one as it is: pass only the fields you mean to move. Safe when you do not hold the whole spec - to raise an app's memory, pass memory_mb and nothing else, and its port, health path and whether it is published are untouched. The app must already be on the box; set_app is what creates one. Every field means what it does on set_app. The box pulls the image and swaps the container the same way. |
remove_app destructive | Stop an app and remove it from the box. Its data directory stays; its environment goes. An app imported from a repository and still waiting for its first build is nothing on the box yet: only its connection goes. |
set_app_env | Replace one app's environment, as a whole: what is not in the mapping is gone. To add or change some variables and keep the rest, use update_app_env instead. The machine fetches the secrets afresh and the app restarts with them. Values are secrets and are never read back; list_apps shows the names. |
update_app_env | Add, overwrite or remove some of one app's environment variables and leave every other one as it is: `set` maps names to new values, `unset` lists names to remove. Safe when you do not hold the whole environment, which nothing reads back. The machine fetches the secrets afresh and the app restarts with them. |
deploy_app | Pull the app's image again, tag unchanged or not, and restart it: what to call after pushing a new build to the tag the app already carries. |
app_logs read | The last lines of one app's log, read from the machine now (up to 1000). |
set_pull_registry | The registry credentials every app on the box pulls with, for private images. An empty pull_user means anonymous pulls; an empty pull_token keeps the one on file, which is never read back. For ghcr.io that is a GitHub username and a classic personal access token with read:packages - the only credential GitHub's registry accepts. They are saved either way and tried against an image already on the box where there is one: `verified` false with a `warning` means the registry refused them, null means there was nothing to try them on. |
app_repo read | What an app is connected to on GitHub: the repository, the workflow file, the branch and the image it watches, the latest run of that workflow (queued, running, green or failed), what is deployed right now and which commit it is, and the builds deployed before it (each of which roll_back_app can go back to). `failed` lists green builds whose deploy did not work, newest first, each with the whole error and a run id retry_deploy takes; `behind` is the commit of the newest green build when that is not the one running. `pending_spec` is set only while an imported app is not on the box yet - it is what the first deploy will create the app from, is never applied to an app that already exists, and is null once the app is there. Null when the app is not connected to anything. |
connect_app_repo | Connect an app to the workflow that builds its image. Nothing about the workflow changes: it keeps building and pushing as it does, and a green run of it deploys this app (auto=true) or is only shown here (auto=false, for a workflow that calls deploy_app itself). `image` is the reference the workflow pushes to, and is resolved to a digest at each deploy so what is running can be named and gone back to; empty keeps the app's current image. The repository must be one the Lathe app is installed on: the person connects that on the Account page of the portal, which is the only place it can be done. Connecting deploys nothing by itself. |
list_repos read | The repositories the Lathe app on GitHub can see for this account: what import_repo and connect_app_repo take. The person installs the app and picks repositories on the Account page of the portal, which is the only place it can be done; empty means they have not yet. |
inspect_repo read | What importing a repository would do, before doing it: the workflow that already builds its image and the reference it pushes (or that none does, and a pull request would add one); the Dockerfile's port and health path, or the stack a generated Dockerfile would be for; and, given instance_id, the app it would make there - name, port, health path, memory, image - each of which import_repo takes as an override. |
import_repo | An app from a GitHub repository in one step, the way to put a person's code on a box: the repository is read, the workflow that builds its image is found (or a pull request adds one, with a Dockerfile when the repository has none and its stack is recognised), the app is created with the port and health path the Dockerfile says, and the newest green build is deployed. With no build yet the app waits, on the instance page and in list_apps as `pending`, and the first green run puts it on the box; every green run after that deploys it. Every field of set_app may be given to override what the repository suggests; inspect_repo shows the suggestion first. A private repository's image needs the box's registry credentials (set_pull_registry: for ghcr.io a classic personal access token with read:packages, which is the only credential it accepts); the answer says so when none are saved. The answer carries the job when a deploy started, `pending` with what it waits for otherwise, `pull_request` when one was opened, and `workflow_yaml` to add by hand when one could not be. |
disconnect_app_repo | Stop watching a repository for this app. The app keeps running exactly what it runs; only the automatic deploys and the build history stop. |
deploy_latest_build | Deploy the newest green build of the connected workflow now, whether or not this app deploys automatically: what to call after connecting a repository, after a build that arrived while automatic deploys were off, and after a deploy that failed for a reason you have since put right - a green build is never used up by a deploy that did not work, and no new commit is needed to take it. To deploy a build that is not the newest, retry_deploy names one. |
retry_deploy | Deploy one named green build again after its deploy failed: `run` is the run id from app_repo's history. For a registry credential the box was refused, or a budget that would not fit the app - fix that, then call this, and the build that is already sitting there goes on the box. The bytes recorded for that run are used where the build was pinned, so this deploys the build you named rather than whatever its tag means now. |
roll_back_app | Put a build this app ran before back on the box, by the digest it ran then, so it is the same bytes even though the tag has moved. `run` is the run id from app_repo's history. The app stays connected and the next green build takes over from here. |
restart_engine | Restart one engine on the box; the others are untouched. Connections to it drop for a few seconds. |
reset_engine destructive | Wipe one engine's data and start it empty (an undo snapshot is kept 7 days); the other engines keep running. confirm must be the instance id. |
list_databases read | The databases on an engine: postgres (size, connections) or couchdb (documents, size). |
create_database | Create a database on postgres or couchdb (lowercase letters, digits, underscores). |
drop_database destructive | Drop a database on postgres or couchdb. confirm must be the database's name. Data is gone (the instance's daily backups remain). |
list_extensions read | Postgres extensions installed in a database and the ones this instance offers (pgvector, pg_trgm, pgcrypto, …). |
set_extension | Install (installed=true) or remove a Postgres extension in a database, from the offered list. |
postgres_query | Run one SQL statement on the instance's Postgres and get the rows back. Gated by the SQL console's own mode (set_sql_console): off refuses; `ro` runs it on the box's read-only role, which holds read on everything and nothing else and opens every transaction read-only, so a write is refused by Postgres itself rather than by guessing at your SQL; `rw` runs it as the owner, which can write. `database` defaults to the console's. Rows are capped at 200 and the answer says when it cut. For a question about the database rather than its contents - which role am I connecting as, what is in this table - this is the short way. |
postgres_connections read | Who holds a Postgres connection right now: by role, by what the client calls itself (application_name - the box sets PGAPPNAME to the app and slot, so each of your containers names itself), by state, and by `from` - `box` for a connection your app or ours opened on the machine, `elsewhere` for a client outside it (a laptop, a migration step, a job runner). Each group carries how long the longest has sat in its state and how old the oldest of them is (`oldest_connection_seconds`, from backend_start): an idle time says nothing has touched it, an age says when it was opened, and dating a connection to a container's lifetime needs the age. Against the plan's max_connections, with `ours` counting the box's own (the metrics exporter, the pooler, the SQL console) so a total you did not expect can be told apart from a leak of your own. |
redis_console | Switch the Redis console off, ro or rw. redis_keys / redis_key / redis_command need it on; rw allows writes. |
redis_keys read | Scan Redis keys matching a glob (type, TTL, size), 50 at a time; pass the returned cursor to continue. |
redis_key read | One Redis key's value (up to 200 members of a collection). |
redis_command | Run one Redis command line on the instance (read-only commands unless the console is rw; server management never). |
list_streams read | JetStream streams on the NATS engine: subjects, messages, bytes, consumers. |
create_stream | Create a JetStream stream: name, subjects (e.g. ['orders.>']), retention limits|interest|workqueue, storage file|memory, max_bytes (-1 = none). |
purge_stream destructive | Purge every message from a stream. confirm must be the stream's name. |
delete_stream destructive | Delete a stream and its messages. confirm must be the stream's name. |
list_auth_users read | The people who can sign in to the customer's app, from the auth engine: address, the providers they use, when they were created and last seen, whether they are confirmed or banned. `query` matches the address; 20 a page. |
create_auth_user | Add a user to the auth engine. Without a password they sign in with a link or a code to the address. The address counts as confirmed unless email_confirm is false, in which case they must confirm it before signing in. |
invite_auth_user | Mail somebody an invitation: the auth engine makes the user and sends a link that signs them in. The engine needs an SMTP server set (configure_engine_settings, group mail). |
send_auth_link | Mail somebody who already has an account a sign-in link (magiclink) or a password reset (recovery). |
ban_auth_user | Ban a user (their sessions are refused and no new one opens) or lift the ban with banned=false. Nothing of theirs is lost either way. user_id is the id from list_auth_users. |
delete_auth_user destructive | Delete a user from the auth engine: their account, identities and sessions go, and rows of the customer's own keyed by that id stay. confirm must be their email address. |
rotate_password | Issue a new password for one engine (engine = postgres, redis, …) or for every engine on the box (omit engine). The old connection URLs stop working immediately; the new ones come from get_connection_urls once the job is done. |
set_sql_console | Turn the browser SQL console (reachable only through the portal) off, read-only (ro) or read-write (rw). The console is connected to one database: pass `database` to open it there (default: the owner database). |
upgrade_instance | Move an instance to a higher plan. The prorated difference for the rest of the billing period is charged to the card on file now; expect 5-10 minutes of downtime. Plans move up only. |
list_backups read | Daily backups (newest first) and undo snapshots that can be restored in place. |
restore_backup destructive | Restore a backup or undo snapshot in place: the current data is replaced (a snapshot of it is kept 7 days). Same address, so connection strings keep working. confirm must be the instance id. |
reset_instance destructive | Wipe every engine's data and start empty (a snapshot is kept 7 days). confirm must be the instance id. |
delete_instance destructive | Delete an instance: billing stops, the VM and its backups are destroyed, a final snapshot is kept 7 days. confirm must be the instance id. |
get_job read | A queued, running or finished operation on one of the customer's instances: kind, status (queued / running / done / error) and the error's first line if it failed. |
Creating an instance costs money: for a paid plan the tool returns a checkout URL that a person opens and pays (on a trial plan it verifies the card and the trial starts); the instance is provisioned when the payment page returns. Destructive tools take confirm, the instance id typed exactly, like the portal.
Webhooks
Every event about your instances, posted to an https URL of yours. Add endpoints on the Account page or with POST /webhooks.
Each delivery is a POST with a JSON body {"id", "type", "instance_id", "at", "data"} and three headers: X-Lathe-Event (the type), X-Lathe-Delivery (an id, the same on every retry) and X-Lathe-Signature - sha256= followed by the hex HMAC-SHA256 of the raw body with the endpoint's secret. Verify the signature before trusting the body. Answer any 2xx within 10 seconds; anything else is retried after 1 minute, 5, 30, 2 hours and 12 hours, then given up. An endpoint that fails 20 deliveries in a row is disabled until you enable it again.
Types you will see: created ready allowlist label resize reset restore rotate console engines engine_settings health_alert health_reset verify_ok verify_failed job_failed pg_database_created pg_database_dropped couch_database_created couch_database_deleted nats_stream_created nats_stream_purged nats_stream_deleted auth_user_created auth_user_invited auth_user_banned auth_user_unbanned auth_user_deleted auth_link_sent api_key_created api_key_revoked oauth_granted oauth_disconnected. New types appear as features do; ignore what you do not know.
REST API
Base URL https://app.lathe.live/api/v1 · interactive docs · OpenAPI
curl -H "Authorization: Bearer lathe_…" https://app.lathe.live/api/v1/instances
curl -H "Authorization: Bearer lathe_…" https://app.lathe.live/api/v1/instances/ID/connection
curl -X POST -H "Authorization: Bearer lathe_…" -H "Content-Type: application/json" \
-d '{"tier": "starter", "name": "acme-prod", "tags": ["prod"]}' https://app.lathe.live/api/v1/instances
| Method and path | Purpose |
|---|---|
GET /me · PATCH /me | The account and its profile (what goes on receipts). |
GET /plans · GET /locations | Plans, prices, hardware, locations. |
GET /instances · POST /instances | List; create (returns checkout_url for a paid plan). |
GET /instances/{id} · PATCH · DELETE ?confirm= | Status, usage, monitoring, backups, activity; rename and retag; delete. |
GET /instances/{id}/connection | Connection strings, password included. |
GET /instances/{id}/backups | Daily backups and undo snapshots. |
PUT /instances/{id}/allowlist · PUT …/sql-console | IP allowlist; SQL console off / ro / rw. |
PUT /instances/{id}/engines | The engines on the box (any subset of Postgres, Redis, CouchDB, NATS, Apps and Auth) and their memory budgets; GET /plans lists each engine's floor, port, category and licence. |
POST /instances/{id}/actions/rotate | resize | reset | restore | Queue an operation; answers 202 with a job id. |
GET /jobs/{id} · GET /billing | A job's progress; subscriptions and charges. |
Errors are JSON: {"detail": "…", "code": "…"}. One job runs per instance at a time; a busy instance answers 409. Reset, restore and delete need confirm = the instance id.