Skip to main content

odoosh-mcp

MCP server to administer 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.

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

  • write_scope — each profile carries an allow-list of project names it is permitted to write to (see odoosh_mcp/config.py). A write against a project outside that scope fails before any HTTP request is made. This bounds a running MCP server only when it was started with odoosh-mcp serve --profile <name>. An unpinned server takes the profile from each tool call's own profile argument, so a caller who can list the configured profiles (any caller can — list_available_profiles is always allowed) can pick whichever one's write_scope covers the project it wants. The effective scope of an unpinned server is therefore the union of every configured profile's write_scope, not the scope of any single one of them (issue #31). Pinning also narrows what list_available_profiles discloses: a pinned server reports only its own pinned profile, not every profile configured locally. The pin holds a profile's name, not a snapshot of its settings: an odoosh-mcp profile scope edit made while the server runs applies to it from the next call.
  • 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.
  • 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 write_scope-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 write_scope 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 profile's write_scope, and the grant outlives the session.
  • Nothing here is anonymous. Every action taken through this server is also audited on odoo.sh itself under the human account that owns the configured 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.

write_scope and permissions are managed with odoosh-mcp profile scope and odoosh-mcp profile permissions; run either with no flags to see the current value. Clearing a scope grants writes on every project the session can reach, so it requires an explicit --yes.

Installation

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:

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

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

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 instead:

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:

$ odoosh-mcp run list_available_profiles
{"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:

~/.pyenv/versions/<env-name>/bin/odoosh-mcp run list_available_profiles

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

odoosh-mcp profile add --name my-account --session-id <paste from the browser cookie>
odoosh-mcp profile add --name my-account --from-browser   # equivalent, no paste needed
odoosh-mcp auth login --profile my-account --from-browser   # needs the browser extra
odoosh-mcp auth status --profile my-account

Every profile subcommand (add, remove, scope, permissions, set-default) accepts --profile as an alias for --name -- the same flag auth and run already use, so one spelling works everywhere. Passing both with different values is refused rather than guessed at.

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, and whether odoo.sh's GitHub grant covers the repository routes. 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 (fingerprint, source, created_at, age_days, write_scope) 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.

Profiles are stored in ~/.config/odoosh-mcp/profiles.json (directory mode 700, file mode 600).

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:

{"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:

odoosh-mcp auth authorize-github --profile my-account

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), pinned to the profile it should act as:

odoosh-mcp serve --profile my-account

From the command line directly:

odoosh-mcp run get_project --param project=my-project
odoosh-mcp run list_branches --param project=my-project --json

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

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

odoosh-mcp serve --profile <name> pins the server to one profile for its whole process lifetime: every tool call runs as that profile, and a call naming a different one is refused before any HTTP request is made, whether or not that tool is normally always-allowed (issue #31). Without --profile, every tool still accepts its own optional profile argument per call, exactly as before — but as the Safety model section above explains, that means write_scope bounds nothing across the profiles configured locally. Always pass --profile when registering this server for an agent, naming the one profile that agent should be able to act as. An empty --profile (for instance an unset $ODOOSH_PROFILE in the registration) is refused at startup rather than read as "use the default profile", because the default can be changed underneath a running server with profile set-default.

With the CLI reachable globally (see Making the CLI reachable everywhere), register the server with Claude Code's own CLI:

claude mcp add --scope user odoosh -- odoosh-mcp serve --profile my-account

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):

{
  "odoosh": {
    "type": "stdio",
    "command": "odoosh-mcp",
    "args": ["serve", "--profile", "my-account"],
    "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:

claude mcp add --scope user odoosh -e PYENV_VERSION=odoosh-user -- odoosh-mcp serve --profile my-account

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 profile 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 the profile's session_id 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 List the profiles configured locally via odoosh-mcp profile add (names and settings only).
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_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) -- 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):

{
  "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 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):

{
  "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:

odoosh-mcp run set_branch_stage --profile my-account \
  --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). Have the agent write the parameters to a file instead, and hand the human a short command that points at it:

odoosh-mcp run set_project_settings --profile my-account \
  --params-file /path/to/settings-params.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 for the full --params-file/--params contract.

Development

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.

Metadata

Release files for odoosh-mcp-server 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for odoosh-mcp-server 0.3.0
File Size Uploaded Provenance Actions
odoosh_mcp_server-0.3.0.tar.gz 148.5 kB Without provenance Details for odoosh_mcp_server-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for odoosh-mcp-server 0.3.0
File Interpreter ABI Platform Actions
odoosh_mcp_server-0.3.0-py3-none-any.whl Python 3 none any Details for odoosh_mcp_server-0.3.0-py3-none-any.whl

Total release size: 244.6 kB

Release files / odoosh_mcp_server-0.3.0.tar.gz

Download URL odoosh_mcp_server-0.3.0.tar.gz
Size 148.5 kB
Tags Source
SHA-256 checksum
How to use checksums
6841441251024e6b9d1b8dd0ed91cd7aca584a514a7d2fd2066b6129ffeea614
BLAKE2b-256 checksum
How to use checksums
5128d14e045d2c9d41e3f7b016a8f647551571cac58cd04cd23f2ddfe680f01b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.9

Release files / odoosh_mcp_server-0.3.0-py3-none-any.whl

Download URL odoosh_mcp_server-0.3.0-py3-none-any.whl
Size 96.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
751cd4e418dc4c5ed45df123ef7c1f95669fec621aedb8d38993e142a80257fb
BLAKE2b-256 checksum
How to use checksums
56f48a061a348e20c8b938aa0faf0f45957bfaf799f16a7d0f91f62e65e3e58e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.9

Security for odoosh-mcp-server 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page