Metadata-Version: 2.5
Name: odoosh-mcp-server
Version: 0.4.0
Summary: MCP server to administer odoo.sh projects agentically — stagings, backups, settings, builds, logs, SSH
Project-URL: Homepage, https://git.vauxoo.com/hugho-ad/odoosh-mcp-server
Project-URL: Repository, https://git.vauxoo.com/hugho-ad/odoosh-mcp-server
Project-URL: Documentation, https://git.vauxoo.com/hugho-ad/odoosh-mcp-server/-/blob/main/README.md
Project-URL: Issue Tracker, https://git.vauxoo.com/hugho-ad/odoosh-mcp-server/-/issues
Author-email: Hugo Adan <hugo@vauxoo.com>
Maintainer-email: Hugo Adan <hugo@vauxoo.com>
License: MIT
License-File: LICENSE
Keywords: agentic,ai,ai-tools,claude,claude-desktop,cursor,erp,json-rpc,llm,mcp,mcp-server,model-context-protocol,odoo,odoo-mcp,odoo-sh,odoosh,ssh,vscode
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Requires-Dist: click>=8.0.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: mcp[cli]<2.0.0,>=1.0.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: browser
Requires-Dist: browser-cookie3>=0.19.1; extra == 'browser'
Provides-Extra: dev
Requires-Dist: build>=1.0.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Requires-Dist: twine>=5.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# odoosh-mcp

MCP server to administer [odoo.sh](https://www.odoo.sh) projects agentically: create stagings,
download backups, manage settings/collaborators/submodules, run code profiling (flamegraphs),
read metadata (production DB size, worker count, staging slots), consume audit logs, manage
builds, tail logs, and reach the SSH-only operations (restart, raw logs, SQL) — all through one
permission-gated tool surface, inspired by the structure of
[odoo-mcp-multi](https://git.vauxoo.com/nhomar/mcp.odoo).

> **Status:** v0.0 in active development. See
> `docs/superpowers/specs/2026-09-04-odoosh-mcp-v0-design.md` for the full design and
> `docs/superpowers/plans/` for the task-by-task implementation plan.

## Why this exists

The odoo.sh web UI has no public API. Every route this server calls was reverse-engineered from
the platform's own OWL frontend bundle and verified live against a disposable trial project. Full
methodology and the 58-route catalog live in `docs/00-discovery.md` and `docs/01-api-surface.md`.

Three facts shape the whole design (details in `docs/02-write-operations.md` and
`docs/03-spikes.md`):

1. **Two HTTP planes, two credentials** — the control plane (`www.odoo.sh/app/*`, cookie auth)
   and the worker plane (`<build.worker_url>/paas/*`, per-project access-token auth) do not share
   credentials.
2. **Writes lie about success, and there is no single way to confirm one.** A write can return
   HTTP 200 with a `null` body while the change never applied — but live spikes proved the audit
   log does not cover every route either (docs/03-spikes.md §7). This server uses three
   confirmation strategies, matched per operation family: settings writes and branch stage
   changes are confirmed against `audit_logs` growth; branch-lifecycle writes that create,
   rebuild, merge or delete a branch, backup, submodule, profiler and collaborator writes are
   re-verified by re-reading the affected resource's own listing (the branch's own
   `history`/`exists`, the project's `backups`, `get_settings().submodules`, `flamegraph/list`,
   `get_settings().users`) — `rebuild` was corrected onto this path 2026-09-18 after live proof it
   is not audited (issue #24, docs/04-platform-facts.md §5); for submodules and
   the profiler because live spikes proved there is no audit trace, for backups because the
   listing is direct evidence the dump exists, and for collaborators because audit coverage was
   never verified either way and an unverified assumption is not a confirmation; SSH-plane
   operations (`restart_build`, `tail_log`, `search_log`, `run_sql`, `ssh_exec`) can only be
   confirmed by the SSH command's own stdout and exit code, since `audit_logs` records nothing
   more specific than "a shell was opened."

   **A write that its strategy could not confirm is returned as `success: false`**, with the
   evidence gathered so far under `data`. An unconfirmed write did not happen, whatever the HTTP
   200 said, so it is never reported as a success with a flag buried inside it.
3. **Restart and raw log tailing have no HTTP route; SQL needs no special access route either.**
   odoo.sh's own UI tells you to run `odoosh-restart` in the webshell for the former. For SQL,
   `odoosh-sql-access` turned out to be for external BI-tool access on dedicated servers only
   (docs/03-spikes.md §4) — the build's own shell already exports `PGDATABASE`/`PGUSER`/
   `PGPASSWORD`/`PGHOST`, so `run_sql` simply runs `psql -c "<sql>"` over the same SSH session on
   any project tier.

## Safety model

Since 0.4.0 a running server has **one session and one write envelope, both fixed when it
starts**. No tool argument selects another session or widens the envelope (issue #33,
`docs/05-profiles-reconsidered.md`).

- **Sessions grant no authority.** A session is one odoo.sh `session_id` cookie, its
  provenance, and an optional **cap** that can only narrow what the ceiling allows for that
  cookie. Several may be stored, because odoo.sh keeps the GitHub grant per session
  (`docs/04-platform-facts.md` §1): two browsers really are two credentials.
- **The write ceiling** — one allow-list of project names for the whole machine, at the top of
  the config file. An **empty ceiling permits no project-level write**; "every project" is the
  explicit entry `["*"]`, written only by `odoosh-mcp scope clear --yes`. Only the human CLI
  changes the ceiling (`odoosh-mcp scope add|remove|show|clear`).
- **The envelope** — what one server process runs with: its session plus the ceiling, narrowed
  by `serve` flags. `--write-scope PROJECT` (repeatable) keeps only those projects;
  `--read-only` or `--allow TIER` (repeatable) keeps only those tiers. A flag asking for anything
  outside the ceiling is a startup error, never a widening. When the session has a cap, the
  envelope is the ceiling intersected with the cap, on every path, flagged or not. A session
  migrated from 0.3.x is capped to the projects and tiers the 0.3.x profiles folded into it could
  write with its cookie (see [Upgrading a 0.3.x config](#upgrading-a-03x-config)). A write against a project outside the
  envelope fails before any HTTP request is made; the refusal names the project and says the
  envelope is fixed for this server, and nothing more -- no other session, no command.
- **Risk tiers** — every tool is registered in `TOOL_REGISTRY` with one of `read`, `write_safe`
  (reversible, no side effect outside the project), `write_external` (touches something outside
  odoo.sh itself, e.g. GitHub, a subscription, another person's access), `destructive`
  (irreversible), or `ssh_exec` (unrestricted shell access). See the tool catalog below for each
  tool's tier. The permissions ceiling (`odoosh-mcp permissions show|set`) limits which tiers any
  server may call; it defaults to every tier, and widening it requires `--yes`.
- **`confirm=True`** — every `write_external`, `destructive`, and `ssh_exec` tool refuses to run
  without an explicit `confirm=True` parameter; omitting it always fails safely with no side
  effect. `run_sql` is the one exception worth calling out: it is registered `write_safe` (opening
  a psql session on a build is a write-tier capability, and both paths are envelope-gated), it
  defaults to read-only by running psql with `PGOPTIONS=-c default_transaction_read_only=on` so
  the *connection* is read-only, and it only requires `confirm=True` when called with
  `read_only=False`. That guard is best-effort, not a sandbox — SQL that resets the GUC itself
  escapes it, which is exactly why the tool is scope-gated as well.
- **Account-level tools take no `project`, so the write envelope cannot restrain them.**
  `add_ssh_key` is therefore `write_external` and requires `confirm=True`: the key it registers
  grants SSH on the build of every project the account can reach, including projects
  deliberately left out of the envelope, and the grant outlives the session. A `--read-only`
  server refuses it along with every other write tier.
- **Nothing here is anonymous.** Every action taken through this server is also audited on
  odoo.sh itself under the human account that owns the session's `session_id` cookie — an agent
  operating this MCP signs with that person's name on the platform's own audit log
  (docs/03-spikes.md §6), whether or not that particular write happens to show up in
  `get_audit_logs`.

What the envelope is not: a wall against an agent that also has a shell. Anything that can run
`odoosh-mcp scope add` or edit the MCP client's registration can widen the next server. The
envelope stops the confused case (the wrong project, a stale default) and makes every widening a
visible human step outside the tool surface rather than a tool argument.

This deliberately diverges from [odoo-mcp-multi](https://git.vauxoo.com/nhomar/mcp.odoo), where a
per-call profile chooses among unrelated Odoo instances: here a per-call profile would choose the
caller's own authority, and callers do not choose their own authority.

## Installation

```bash
pip install odoosh-mcp-server
```

The distribution is `odoosh-mcp-server`; it installs two console scripts, `odoosh-mcp` (the
canonical one, used throughout this README) and `odoosh-mcp-server`, which are the same CLI; the
importable module is `odoosh_mcp`. PyPI already hosts an unrelated `odoo-sh-mcp` -- a different
tool, which reads ORM metadata over XML-RPC rather than administering the platform -- and PyPI
treats the two names as the same once separators are stripped, so this one carries the suffix.

Importing the session cookie from a local browser needs one extra dependency, declared as the
optional `browser` extra:

```bash
pip install "odoosh-mcp-server[browser]"
```

Everything else works without it. To install an unreleased revision instead:

```bash
pip install "git+ssh://git@git.vauxoo.com/hugho-ad/odoosh-mcp-server.git@main"
```

### Making the CLI reachable everywhere

A plain `pip install` only puts `odoosh-mcp` on the PATH of whatever environment received it. If
that environment is a pyenv virtualenv, the command works only while that virtualenv is active --
from any other directory, or from a tool that just runs `odoosh-mcp` by name (a script, an agent
harness, an MCP client), the pyenv shim reports `pyenv: odoosh-mcp: command not found`, even though
`pyenv versions` lists the environment that has it right there. Every command from here on assumes
`odoosh-mcp` resolves with no shell setup, so install with [pipx](https://pipx.pypa.io) instead:

```bash
pipx install odoosh-mcp-server
```

This puts `odoosh-mcp` and `odoosh-mcp-server` on `~/.local/bin`, in their own isolated virtualenv,
reachable from any directory with no `PYENV_VERSION`, no `pyenv activate`, and no per-command
environment variable. Verified from `$HOME`, outside any checkout of this repository:

```console
$ odoosh-mcp run list_sessions
{"success": true, "data": [...]}
```

If `odoosh-mcp` still resolves to a pyenv shim after installing with pipx, `~/.local/bin` is not
ahead of `~/.pyenv/shims` on `PATH` -- `pipx ensurepath` fixes that (restart the shell afterwards).

To keep the CLI inside a pyenv environment instead of installing a second, pipx-managed copy,
point at that environment's own binary directly rather than juggling `PYENV_VERSION` plus the
shim -- it is one fewer moving part and does not depend on shell PATH order at all:

```bash
~/.pyenv/versions/<env-name>/bin/odoosh-mcp run list_sessions
```

`odoosh-mcp --version` reports the installed distribution's version, useful for confirming which
release is running at a cold start or when filing a bug report.

## Configuration

```bash
odoosh-mcp auth login --from-browser          # first session, stored as "default"; needs the browser extra
odoosh-mcp session add work --session-id <paste from the browser cookie>
odoosh-mcp session add work --from-browser    # equivalent, no paste needed
odoosh-mcp auth status --session work
odoosh-mcp scope add my-project other-project  # nothing is writable until you do this
odoosh-mcp scope show
```

`session add|list|remove|rename|set-default` manage the stored cookies; `list` shows a
six-character fingerprint per session, never the cookie. A cookie already stored under one
session name is refused under a second: one cookie is one session. `auth login` with no
`--session` refreshes the default session, and creates one named `default` only when none exists
yet.

`scope add|remove|show` and `scope clear --yes` change the write ceiling; `permissions show|set`
the tier ceiling. `session scope --session NAME --add|--remove|--show` edits one session's cap,
and `--clear --yes` drops it; `session permissions --session NAME` edits its tier cap, with
`--yes` to widen. `session list` and `scope show` print every cap. A session you add has no cap,
and `session add` says so: the ceilings alone bound it. Every change applies to servers started
afterwards: a running server keeps the envelope it started with.

odoo.sh issues no API token: signing in is GitHub OAuth against odoo.sh's own OAuth application,
and the credential it produces is a session cookie. The callback lands on odoo.sh rather than on a
port this server could listen on, so there is no way for the server to run the login itself. What
it can do is read that one cookie back from the browser that already holds it (`--from-browser`
reads only the `session_id` cookie, only for the odoo.sh host), and tell you when it has gone
stale.

`auth status` reports whether the session is alive, how old it is, where it came from, whether
odoo.sh's GitHub grant covers the repository routes, and the envelope a server on it would run
with. It never prints the cookie — only a six-character fingerprint, which is enough to confirm
that a re-login actually replaced it.

`auth_status` always answers with a status, even when the session is dead: it returns
`success: true` with `alive: false`, the fields it can still supply locally (`session`,
`fingerprint`, `source`, `created_at`, `age_days`, `write_scope`, `permissions`) and a
`remediation`, rather than collapsing into an error envelope. **Read `alive`, not `success`, to
know whether the session works** — this is the one exception in the whole server, because a
diagnostic tool whose entire job is reporting session health must not itself fail on the one
condition it exists to report. `success: false` from `auth_status` means the tool could not answer
at all (a transport failure). Every other tool, including `check_auth` (the plain "is the cookie
alive right now" check) and `get_project`, still reports `success: false` on an expired session,
unchanged.

Sessions and the ceilings are stored in `~/.config/odoosh-mcp/profiles.json` (directory mode 700,
file mode 600), config format 2.

### Upgrading a 0.3.x config

0.4.x reads a 0.3.x (format 1) file transparently and prints one line to stderr saying so;
`odoosh-mcp config migrate` rewrites it in format 2 and leaves `profiles.json.format1.bak` (mode
600) beside it. Any command that has to save (`auth login`, `session ...`, `scope ...`) performs
the same rewrite and says so. 0.5.0 reads format 2 only.

- Profiles whose cookie, base URL and SSH key are byte-identical become one session, named after
  the default profile when it is among them; the other names are kept as aliases for
  `--profile`. `--session` takes a session name only: `--session grupotenerife` is refused rather
  than widened to the whole session it folded into.
- The write ceiling is the union of every profile's `write_scope`, the most any caller of an
  unpinned 0.3.x server could reach by naming the right profile. If any profile had an empty
  `write_scope` (which meant every project), the ceiling is `["*"]` and every start prints a
  warning until you run a `scope` command once. A literal `"*"` inside a 0.3.x `write_scope`
  matched only a project named `*`, so it is dropped with a note, never read as the wildcard.
- **Each migrated session keeps its 0.3.x pairing, as its cap.** In 0.3.x a cookie wrote a
  project only through a profile holding both, so migration caps each session to the projects of
  the profiles folded into it, on every path, including an unflagged `serve`. Its tier cap is the
  write tiers all of those profiles shared, plus `read` if any could read. The permissions
  ceiling is the union of every profile's tiers; the tiers a session gives up are printed on
  migration and at every start. The cap is stored on the session (`sessions.NAME.write_scope` and
  `permissions`), not in the 0.3.x aliases, so it stays when 0.5.0 removes them. A session you
  add after the upgrade has no cap and is bounded by the ceilings alone.
- **Giving a migrated session a new project takes two commands**, because both bounds must allow
  it: `odoosh-mcp scope add P`, then `odoosh-mcp session scope --session S --add P`. Until the
  second one, `serve --session S --write-scope P` is refused at startup, and the refusal says the
  cap is set on the session.
- **No 0.3.x registration or command gains authority.** Each old profile name is recorded with its
  own scope and tiers: `serve --profile X` and `run --profile X` still mean X's 0.3.x scope and
  tiers (within the ceilings), and a bare `run TOOL` still means the default profile's scope until
  you run `session set-default`. A 0.3.x file with no valid `default_profile` gets no default
  session, so a bare `run` or `serve` is refused, as the bare `run` was in 0.3.x. The old `profile`
  commands still work in 0.4.x, print one deprecation line, and change only the profile they
  name, moving its session's cap by the same step: `profile scope` edits that profile's scope
  (growing the ceiling as far as it needs), `profile permissions` its tiers (never the ceiling),
  `profile remove` removes that one name, and `auth login --profile X` on a folded X gives X its
  own session when the cookie differs, rather than replacing the cookie of every name sharing it.

### When a tool answers with a remediation

An expired session and an insufficient GitHub grant both come back as an ordinary error envelope
carrying a `remediation` object that names the fix:

```json
{"success": false, "error": "The token does not provide the required scope ...",
 "route": "/app/branch/5118230/fork",
 "remediation": {"action": "authorize_github",
                 "url": "https://github.com/login/oauth/authorize?...",
                 "scopes": ["read:user", "user:email", "repo"],
                 "hint": "call authorize_github, then retry"}}
```

`create_staging`, `merge_branch` and `add_submodule` need GitHub scopes that a plain odoo.sh login
does not request, and the scope set differs per route, so the URL always comes from odoo.sh's own
error rather than from a constant in this package. Run:

```bash
odoosh-mcp auth authorize-github --session work
```

It prints the scopes odoo.sh is asking GitHub for, opens the authorization page after you confirm,
and then polls until the grant lands. The MCP tool of the same name never opens a browser unless
it is called with `open_browser=true` — an agent gets the URL to hand to a human, not control of
somebody's desktop.

## Usage

As an MCP server (stdio), with the default session and the whole write ceiling:

```bash
odoosh-mcp serve
```

From the command line directly:

```bash
odoosh-mcp run get_project --param project=my-project
odoosh-mcp run list_branches --param project=my-project --json
odoosh-mcp run list_branches --session work --param project=my-project
```

Parameters for a `run` command can also come from a JSON file or an inline JSON string, instead of
repeated `--param`:

```bash
odoosh-mcp run delete_branch --params-file params.json
odoosh-mcp run delete_branch --params '{"project": "my-project", "branch": "feature-old", "confirm": true}'
```

Useful when an agent prepares a confirm-gated write for a human to run by hand: writing the JSON
once avoids retyping a long `--param ... --param ...` line, where terminal line-wrapping has been
observed to insert stray control characters into a pasted value. `--params-file` and `--params` are
mutually exclusive; an explicit `--param key=value` overrides the same key from either; `confirm`
must still be an explicit key in the JSON -- it is never implied. Every `--param` value (and every
file/inline JSON value) is rejected if it contains a raw control character (`\r`, `\n`, `\t`, ...)
rather than silently passed through.

## Use from an MCP client

An MCP client registers a server once, with fixed arguments, so the registration line is where a
server's authority is chosen -- and it is readable in `claude mcp list` without opening any file.
Every tool call on that server runs with the same session and envelope. Tools take no `profile`
argument; a call that still passes one (a 0.3.x habit) is refused with an explanation rather than
silently run under the server's session.

```bash
# the default session with the write ceiling (a migrated session: within its 0.3.x pairing)
claude mcp add --scope user odoosh -- odoosh-mcp serve

# a read-only agent over the same account
claude mcp add --scope user odoosh-ro -- odoosh-mcp serve --read-only

# one client engagement: this project only, on the browser session that holds its GitHub grant
claude mcp add --scope user odoosh-trinitate -- odoosh-mcp serve --session trinitate --write-scope trinitate-v18
```

`serve` prints the envelope it bound to stderr at startup (`serving session 'trinitate', write
envelope ['trinitate-v18'], tiers full`), which an MCP client's server log shows. An empty
`--session` (for instance an unset `$ODOOSH_SESSION` in the registration) is refused at startup
rather than read as "use the default session". `list_sessions` returns the server's own session
and envelope only, never the other sessions on the machine and never a cookie.

A 0.3.x registration `serve --profile X` keeps working through 0.4.x as a deprecated alias for
`--session <the session X folded into> --write-scope <X's 0.3.x write_scope>`; re-register it with
the explicit flags before 0.5.0.

With the CLI reachable globally (see
[Making the CLI reachable everywhere](#making-the-cli-reachable-everywhere)), register the server
with Claude Code's own CLI:

```bash
claude mcp add --scope user odoosh -- odoosh-mcp serve --write-scope my-project
```

Verified: this writes the server into `~/.claude.json`, under the top-level `mcpServers` key --
**not** `~/.claude/.mcp.json`, which is not a file Claude Code reads for server registration.
`--scope user` is what makes the server available in every project; the default `--scope local`
registers it for the current project only.

The equivalent JSON, for a client that takes its MCP config as a file rather than through a CLI
(this is the exact shape `claude mcp add` writes):

```json
{
  "odoosh": {
    "type": "stdio",
    "command": "odoosh-mcp",
    "args": ["serve", "--write-scope", "my-project"],
    "env": {}
  }
}
```

If the CLI is not installed with pipx and instead lives in a pyenv environment, pin it in `env`
rather than relying on `PATH`/shim resolution to pick the right one:

```bash
claude mcp add --scope user odoosh -e PYENV_VERSION=odoosh-user -- odoosh-mcp serve --write-scope my-project
```

or point `command` directly at that environment's own binary
(`~/.pyenv/versions/<env-name>/bin/odoosh-mcp`), which needs no `env` block at all and does not
depend on `PATH` order.

## Tool catalog

Generated from `TOOL_REGISTRY` by `scripts/render_tool_catalog.py` — re-run that script and paste
its output here whenever a tool is added, renamed, or has its tier/confirm gate changed, so this
table cannot drift from what the server actually registers.

One side effect worth knowing before calling `download_backup` on a storage-constrained project:
triggering a dump also leaves a `manual` backup entry on odoo.sh (observed live 2026-09-05), which
counts against storage until it expires on its own — there is no route to delete it.

| Tool | Tier | Confirm | Description |
|---|---|---|---|
| `add_collaborator` | write_external | yes | Invite `github_username` at `access_level` ("admin", "tester" or "developer"). Requires confirm=True. |
| `add_ssh_key` | write_external | yes | Register `public_key` as an SSH key on the account. Requires confirm=True. |
| `add_submodule` | write_external | yes | Add submodule `submodule_url` (branch `submodule_branch`) at `path` on `branch`. Requires confirm=True. |
| `auth_status` | read | no | Report whether this server's session can talk to odoo.sh right now, and with what authority. |
| `authorize_github` | read | no | Check odoo.sh's GitHub grant and return the URL that widens it when it is insufficient. |
| `check_auth` | always_allowed | no | Verify this server's session cookie is still live against odoo.sh. |
| `clean_flamegraphs` | destructive | yes | Delete every captured flamegraph file for `build_id`. Requires confirm=True. |
| `create_backup` | write_safe | no | Trigger a manual backup of `branch`'s current build and wait for it to appear. |
| `create_project` | write_external | yes | Create a new odoo.sh project from GitHub repo `owner/repository`. Requires confirm=True. |
| `create_staging` | write_external | yes | Fork `from_branch` into a new branch `name` at the given `stage`. Requires confirm=True. |
| `create_submodule_deploy_key` | write_safe | no | Create a deploy key for `submodule_url` so the project's repo can pull that private submodule. |
| `delete_branch` | destructive | yes | Permanently delete `branch` from the project. Requires confirm=True. |
| `delete_ssh_key` | destructive | yes | Remove SSH key `key_id` from the account. Requires confirm=True. |
| `delete_submodule_deploy_key` | destructive | yes | Delete submodule deploy key `submodule_id` (from `create_submodule_deploy_key`'s `key.id`). |
| `dismiss_notification` | write_safe | no | Dismiss notification `notification_id` on the project. |
| `download_backup` | write_safe | no | Trigger a downloadable dump of `branch`'s build and save it to `dest_dir`. |
| `download_flamegraph` | read | no | Download flamegraph `name` (from `list_flamegraphs`) for `build_id` into `dest_dir`. |
| `get_account_profile` | read | no | Fetch the odoo.sh account profile (name, SSH keys on file, etc.) for the signed-in user. |
| `get_audit_logs` | read | no | List the project's audit log, newest entries first, capped at `limit`. |
| `get_branch` | read | no | Fetch one branch's record, resolved from the full `list_branches` listing. |
| `get_branch_history` | read | no | Fetch a branch's build/commit history. |
| `get_branch_settings` | read | no | Fetch a branch's settings as odoo.sh reports them right now. |
| `get_build` | read | no | Fetch one build's record plus its install/runtime errors, by id. |
| `get_monitoring` | read | no | Uptime/status plus the URL of odoo.sh's own HTML monitoring page. |
| `get_project` | read | no | Fetch one project's identity plus storage/worker/staging-slot metadata. |
| `get_project_settings` | read | no | Fetch the project's full settings dict, including its `repository`, `submodules` and `users` sections. |
| `get_project_status` | read | no | Fetch the project's current status (the data odoo.sh's own status/monitoring page reads). |
| `import_github_ssh_keys` | write_safe | no | Import the account's GitHub-registered SSH public keys into odoo.sh. |
| `list_available_profiles` | always_allowed | no | Deprecated alias of list_sessions, removed in 0.5.0. Same payload plus a `deprecated` key. |
| `list_backups` | read | no | List the project's backups (daily, remote, manual, update, restore, import, upgrade, other). |
| `list_branches` | read | no | List the project's branches, optionally filtered to one stage (production/staging/dev). |
| `list_builds` | read | no | List builds per branch, newest first. `limit` counts builds per branch; `branch_limit` caps the branches. |
| `list_collaborators` | read | no | List the project's GitHub-linked collaborators, from `get_settings().users`. |
| `list_database_users` | read | no | List the Odoo database users on `branch_build_id`'s own database (worker-plane HTTP). |
| `list_flamegraphs` | read | no | List the flamegraph files already captured on `build_id` (worker-plane HTTP). |
| `list_logs` | read | no | List the log files available on `branch`'s current build (worker-plane HTTP, no SSH). |
| `list_notifications` | read | no | List the project's current notifications (e.g. "Database dump ready" download prompts). |
| `list_projects` | read | no | List every project (repo) visible to this account. |
| `list_sessions` | always_allowed | no | Describe the one session this server runs with, and the write envelope bound to it. |
| `list_submodules` | read | no | List the project's submodules and their deploy keys, from `get_settings().submodules`. |
| `merge_branch` | write_external | yes | Merge `source_branch` into `target_branch` (optionally rebasing). Requires confirm=True. |
| `rebuild_branch` | write_safe | no | Trigger a fresh build of `branch` from its current commit. |
| `restart_build` | write_external | yes | Restart `service` ("http" or "cron") on `branch`'s build via `odoosh-restart` over SSH. Requires confirm=True. |
| `restore_backup` | destructive | yes | Restore `target_branch_id` (production or staging only) to a prior backup. Requires confirm=True. |
| `revoke_collaborator` | destructive | yes | Revoke collaborator `user_access_id`'s access. Requires confirm=True. |
| `run_sql` | write_safe | no | Run arbitrary SQL on the build's Postgres via `psql -c` over SSH. |
| `search_log` | read | no | Grep `pattern` in log `name` over SSH, returning the last `lines` matches. |
| `set_branch_settings` | write_safe | no | Write one or more settings (e.g. `push_behavior`, `test_tags`, `modules`) on `branch`. |
| `set_branch_stage` | write_external | yes | Move a branch to dev/staging/production. Requires confirm=True; demoting production needs confirm_project_name. |
| `set_collaborator_access` | write_external | yes | Set collaborator `user_access_id` to `access_level` ("admin", "tester" or "developer"). Requires confirm=True. |
| `set_project_settings` | write_external | yes | Write project-level settings (e.g. worker/storage limits, staging slot count). Requires confirm=True. |
| `ssh_exec` | ssh_exec | yes | Run an arbitrary shell `command` on `branch`'s build over SSH. Requires confirm=True. |
| `start_profiler` | write_safe | no | Start flamegraph profiling on `build_id`. |
| `stop_profiler` | write_safe | no | Stop flamegraph profiling on `build_id`, producing a downloadable flamegraph file. |
| `tail_log` | read | no | Tail the last `lines` of log `name` (e.g. "odoo", "install") over SSH. |
| `wait_for_build` | read | no | Block, polling, until `branch`'s current build reaches a terminal status. |

## Running this under an agent harness

Every `write_external`, `destructive`, and `ssh_exec` tool refuses to run without `confirm=true`
(see [Safety model](#safety-model)) -- and `confirm=true` in a command line is exactly what an
agent harness's own safety classifier tends to react to, independently of this server's own gate.
Verified live in Claude Code's auto mode: `odoosh-mcp run set_branch_stage ... --param
confirm=true` is denied by the classifier under "Interfere With Workloads" regardless of whether a
human already approved the action -- and the agent has no way to grant itself the permission it
needs.

### Give each confirm-gated tool its own allow rule

Add one `Bash` permission rule per tool -- not a blanket one -- to `.claude/settings.json` (project
scope) or `~/.claude/settings.json` (every project):

```json
{
  "permissions": {
    "allow": [
      "Bash(odoosh-mcp run set_branch_stage:*)",
      "Bash(odoosh-mcp run restart_build:*)"
    ]
  }
}
```

**Never** `"Bash(odoosh-mcp run:*)"`. That single rule also allows `delete_branch`,
`restore_backup`, and every other `destructive` tool in the [catalog](#tool-catalog) above, for no
extra benefit -- the entire point of naming the tool is that approving one write does not approve
them all. The rule must match the command text verbatim, prefix included: a rule written for
`odoosh-mcp run ...` does not match `PYENV_VERSION=odoosh-user odoosh-mcp run ...` or vice versa
(verified live by switching between the pipx and pyenv-pinned invocation styles), so keep both
forms if either might be used.

Running this as an MCP server rather than a CLI needs the equivalent `mcp__<server-name>__<tool>`
rule instead, where `<server-name>` is whatever name the server was registered under (`odoosh` in
[Use from an MCP client](#use-from-an-mcp-client)):

```json
{
  "permissions": {
    "allow": [
      "mcp__odoosh__set_branch_stage",
      "mcp__odoosh__restart_build"
    ]
  }
}
```

### When the agent still can't run it: hand the command to the human

Even with the rule in place, some environments deny a confirm-gated write outright -- a stricter
harness policy, a permission mode the agent has no way to escalate out of, a human declining in
the moment. There is no workaround for that in odoosh-mcp, and there should not be: a denied
`confirm=true` is the harness doing its job, not a bug in this server. The designed fallback is for
the agent to work out every parameter and hand the human one command to paste:

```bash
odoosh-mcp run set_branch_stage --session work \
  --param project=my-project --param branch=my-feature --param stage=staging \
  --param confirm=true
```

For a write with many parameters, or one whose value is itself JSON (`set_project_settings`'s
`settings=`), hand-building a chain of `--param key=value` gets unwieldy -- and is exactly the kind
of long, wrapped command line that picks up stray control characters when pasted (see
[Usage](#usage)). Have the agent write the parameters to a file instead, and hand the human a short
command that points at it:

```bash
odoosh-mcp run set_project_settings --session work \
  --params-file /path/to/settings-params.json
```

```json
{
  "project": "my-project",
  "settings": {"test_tags": ":AccountTests"},
  "confirm": true
}
```

`confirm` still has to be an explicit key in the file -- pointing at a file never implies or
bypasses the confirm gate. See [Usage](#usage) for the full `--params-file`/`--params` contract.

## Development

```bash
pyenv virtualenv 3.12 odoosh-mcp
pyenv activate odoosh-mcp
pip install -e ".[dev]"
pytest
ruff check odoosh_mcp/
```

Integration tests that hit a live odoo.sh project are opt-in and read-only by default — see
`tests/integration/` and the design spec §10. `tests/integration/test_live_auth.py` checks the
authentication surface without writing anything or opening a browser.
