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.mdfor the full design anddocs/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):
-
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. -
Writes lie about success, and there is no single way to confirm one. A write can return HTTP 200 with a
nullbody 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 againstaudit_logsgrowth; 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 ownhistory/exists, the project'sbackups,get_settings().submodules,flamegraph/list,get_settings().users) —rebuildwas 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, sinceaudit_logsrecords 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 underdata. 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. -
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-restartin the webshell for the former. For SQL,odoosh-sql-accessturned out to be for external BI-tool access on dedicated servers only (docs/03-spikes.md §4) — the build's own shell already exportsPGDATABASE/PGUSER/PGPASSWORD/PGHOST, sorun_sqlsimply runspsql -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_idcookie, 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 byodoosh-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
serveflags.--write-scope PROJECT(repeatable) keeps only those projects;--read-onlyor--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). 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_REGISTRYwith one ofread,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), orssh_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— everywrite_external,destructive, andssh_exectool refuses to run without an explicitconfirm=Trueparameter; omitting it always fails safely with no side effect.run_sqlis the one exception worth calling out: it is registeredwrite_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 withPGOPTIONS=-c default_transaction_read_only=onso the connection is read-only, and it only requiresconfirm=Truewhen called withread_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_keyis thereforewrite_externaland requiresconfirm=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-onlyserver 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_idcookie — 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 inget_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, 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
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_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:
~/.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
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.--sessiontakes a session name only:--session grupotenerifeis 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 emptywrite_scope(which meant every project), the ceiling is["*"]and every start prints a warning until you run ascopecommand once. A literal"*"inside a 0.3.xwrite_scopematched 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, plusreadif 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_scopeandpermissions), 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, thenodoosh-mcp session scope --session S --add P. Until the second one,serve --session S --write-scope Pis 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 Xandrun --profile Xstill mean X's 0.3.x scope and tiers (within the ceilings), and a barerun TOOLstill means the default profile's scope until you runsession set-default. A 0.3.x file with no validdefault_profilegets no default session, so a barerunorserveis refused, as the barerunwas in 0.3.x. The oldprofilecommands 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 scopeedits that profile's scope (growing the ceiling as far as it needs),profile permissionsits tiers (never the ceiling),profile removeremoves that one name, andauth login --profile Xon 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:
{"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 --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.
SSH and the firewall
Since 2026-09-21 odoo.sh only accepts SSH to production and staging builds from an authorized
public IPv4; a valid key is no longer enough. An unauthorized machine sees a bare
Connection timed out, so every SSH tool (ssh_exec, tail_log, search_log, run_sql,
restart_build) turns that into success: false with a remediation naming both ways in:
authorize_my_ip(project,branch): the UI's "Allow my IP" button. Authorizes the IP odoo.sh sees making the call, for 1 hour. No route lists these grants, so the result carries the platform's echo of the IP andverifiable: false, not a confirmation.set_ip_whitelist(project,addand/orremove,confirm=true): the permanent list in Settings > Firewall, at most 10 IPv4 addresses. It takes a change, not a whole list, so an address you did not name is never dropped, and it is verified by reading the list back.get_ip_whitelistandget_project_settings(underfirewall) show the current list.
Either one takes up to 5 minutes to apply (about 4 measured). A timeout right after
authorizing is expected: wait and retry rather than authorizing again. When a grant runs out, the
build refuses the key (Permission denied (publickey)) for a few minutes before it starts
timing out, so that error comes back naming both an unregistered key and an expiring grant.
odoosh-mcp run authorize_my_ip --param project=acme --param branch=staging-1
odoosh-mcp run set_ip_whitelist --param project=acme --param 'add=["203.0.113.10"]' --param confirm=true
ssh_exec's success now means the command ran: an unreachable build, any other ssh error
(exit 255) and its own timeout (exit 124) are failures, while the command's own non-zero exit is
still data under data.returncode.
Usage
As an MCP server (stdio), with the default session and the whole write ceiling:
odoosh-mcp serve
From the command line directly:
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:
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.
# 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.
The tool list a client sees follows the envelope's tiers: --read-only lists 30 tools (the read
tier plus list_sessions, list_available_profiles and check_auth), --allow read --allow write_safe --allow write_external lists 52, and an unflagged server with a full permissions
ceiling lists all 59. A tool outside the tiers is simply absent, so calling it by name answers
Unknown tool; odoosh-mcp run and any direct call are still refused by the tier check, which
stays the real gate. --write-scope does not shorten the list: a write tool stays listed even
when the scope refuses every project, and the refusal comes at call time.
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), register the server with Claude Code's own CLI:
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):
{
"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:
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.
Shared local server
Every Claude Code session that registers odoosh-mcp serve over stdio starts its own process, at
about 58-66 MB each. serve --http serves the same tools to every session from one process on a
loopback port, and odoosh-mcp service runs it as a systemd user unit (Linux only). One
instance is one envelope: the flags fix its session and write envelope for its lifetime, exactly as
they do for stdio, and nothing a client sends selects another. A second envelope is a second
instance with its own name, port, token and unit. The design and its threat model are in
docs/06-shared-local-server.md, sections 3.5, 6 and 8.
odoosh-mcp service install --allow read --allow write_safe --allow write_external # instance odoosh, port 5011
odoosh-mcp service status # active?, port answering?, token file still 0600?
odoosh-mcp service install ... --dry-run # print the unit, change nothing
odoosh-mcp service uninstall # stop, disable, remove the unit; the token file is kept
service install validates the envelope flags before it writes anything (a tier or project outside
the ceiling fails the install, not every restart of the unit), writes
~/.config/systemd/user/odoosh-mcp-<instance>.service, enables and starts it, waits for the port,
and prints the client line. The flags sit verbatim in ExecStart, so systemctl --user cat odoosh-mcp-odoosh shows the instance's authority in plain text. ExecStart runs the absolute
interpreter of the environment the package is installed in, with -m odoosh_mcp (for pipx,
~/.local/share/pipx/venvs/odoosh-mcp-server/bin/python -m odoosh_mcp serve --http ...), so no
other odoosh-mcp on PATH can shadow it; under uvx it pins uvx --from odoosh-mcp-server==<installed version>. Nothing runs as root. The unit stops at logout; install
prints the loginctl enable-linger $USER line that would change that and never runs it.
What the listening socket is held to, none of it configurable:
--hostmust be exactly127.0.0.1,localhostor::1;0.0.0.0,127.1,::ffff:127.0.0.1andLOCALHOSTrefuse to start.- Every HTTP request needs
Authorization: Bearer <token>, checked before routing, so any path without it answers 401. The token lives in~/.config/odoosh-mcp/local-token-<instance>, one per instance, created 0600; the server prints the file's path, never the token, and refuses to start if the file is readable by group or others.serve --http --rotate-tokenreplaces it (then re-register the client, which holds a copy). - Any
Originheader answers 403 and aHostother than a loopback name with a port answers 421, so a web page -- including one served from another localhost port -- cannot drive the server. --max-concurrency N(default 40) caps tool calls running at once; calls past it queue.
Register it with Claude Code (install prints these lines with your instance's values):
claude mcp remove -s user odoosh
claude mcp add-json -s user odoosh '{"type":"http","url":"http://127.0.0.1:5011/mcp","headers":{"Authorization":"Bearer '"$(cat ~/.config/odoosh-mcp/local-token-odoosh)"'"},"timeout":1200000}'
add-json rather than claude mcp add --header, because add has no flag for the per-server
timeout (milliseconds), and without it long calls fail (below).
The registration keeps the name odoosh, so tools stay mcp__odoosh__<tool> and every
permission rule naming them keeps matching. Checked on Claude Code 2.1.289:
- The token is stored in plain text in
~/.claude.json(mode 0600).claude mcp get odooshprints it in full;claude mcp listdoes not. Do not runmcp geton a shared screen. - Over HTTP, a long tool call needs the per-server
timeout. Without it, a 120 s call failed with "The operation timed out" after about 66 s, while the same call completed over stdio.MCP_TOOL_TIMEOUT=1200000lifts that limit but not a second one: a 920 s call was still aborted at 300 s with "sent no response or progress for 300s", with or without event-stream responses. The registration's"timeout": 1200000alone lifted both: the 920 s call completed. That covers await_for_build, which runs up to 900 s. - After
systemctl --user restart, a running session's next call reconnects by itself, with no/mcpreconnect.
Restarts. systemctl --user restart odoosh-mcp-odoosh stops accepting new connections and
lets calls in flight finish, up to TimeoutStopSec=960, so a wait_for_build survives a restart
and a restart can take as long as the longest call in flight. A call cut anyway (a crash, a kill)
has an unknown outcome, not a failed one: re-read the resource before retrying a write.
A new cookie needs a restart. The service holds the cookie it started with. auth login
prints the systemctl --user restart line for every installed unit and does not run it, because
the restart affects every session using the service.
Upgrading. pipx upgrade odoosh-mcp-server, then odoosh-mcp service install again with the
same flags; it rewrites the unit and restarts it.
Moving from stdio registrations
-
pipx upgrade odoosh-mcp-server, thenodoosh-mcp service install --allow read --allow write_safe --allow write_externalandodoosh-mcp service status. -
Pilot in one directory: the
add-jsonline above with-s localinstead of-s usershadows the user-scope stdio entry there only, with the same tool names. -
Switch: run the
removeandadd-jsonlines above at user scope. New sessions use the service; sessions already running keep their stdio process until they exit. Let them drain rather than killing them, because killing cuts their calls in flight. -
Rollback to the stdio registration:
claude mcp remove -s user odoosh claude mcp add -s user odoosh -- odoosh-mcp serve --allow read --allow write_safe --allow write_external odoosh-mcp service uninstall
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. |
authorize_my_ip |
write_safe | no | Authorize this server's public IP for SSH to the project's builds for 1 hour (unverifiable, see docstring). |
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_ip_whitelist |
read | no | Read the project's permanent firewall list: the IPv4 addresses always allowed to SSH in. |
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_ip_whitelist |
write_external | yes | Add or remove addresses on the project's permanent firewall list, verified by re-reading. 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 --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). 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 --session work \
--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.4.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| odoosh_mcp_server-0.4.3.tar.gz | 239.7 kB | Without provenance | Details for odoosh_mcp_server-0.4.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| odoosh_mcp_server-0.4.3-py3-none-any.whl | Python 3 | none | any | Details for odoosh_mcp_server-0.4.3-py3-none-any.whl |
Total release size: 384.6 kB
Release files / odoosh_mcp_server-0.4.3.tar.gz
| Download URL | odoosh_mcp_server-0.4.3.tar.gz |
|---|---|
| Size | 239.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
678635eb012d75b744433a097c9b203d5858ad333152cbc38efd514ec191a890
|
|
BLAKE2b-256 checksum How to use checksums |
b9f66411689c4739aeada6f02a0e3d8876cb54ba0cd19f6ed783ed3dfb9e3f68
|
| 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.4.3-py3-none-any.whl
| Download URL | odoosh_mcp_server-0.4.3-py3-none-any.whl |
|---|---|
| Size | 144.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9639db75251a54ef182a957c49b84d483b6d7989c68db4656a347428235f9057
|
|
BLAKE2b-256 checksum How to use checksums |
677390ecdfe0cc5cd9bb02ec0638cfc739b7ebb1233ccd47a54f709ebe7e3bd6
|
| 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.4.3
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.