Ads.Onyx MCP Server
An MCP server that gives an AI assistant — Claude Code, Claude Desktop, Cursor — working access to an Ads.Onyx instance: its pipelines, workflows, runs, connections, reports, scripts, schedules, secrets keys and project variables.
It reads widely, writes deliberately, and runs nothing.
pip install ads-onyx-mcp
Licence. This is proprietary software, free to use for holders of an Ads.Onyx licence from Alchimie Data Solutions. Publishing it here makes it easy for customers to install; it does not make it free software. See LICENSE.
What it is
A client-side façade over the Onyx REST API. No business logic is duplicated here: when a capability is missing, the Onyx API gets it, which benefits every channel rather than this one.
The security model is one line:
One process = one user = one tenant = one Onyx instance.
Everything follows from that. The server arbitrates no permission because it needs none: it can
do nothing its configured user could not already do in the Onyx web interface. A user without
Pages.OJobInstances gets a 403 from Onyx, whether the request came from a browser or from a
language model. There is no service account, so there is no confused-deputy problem — the risk
is absent by construction rather than mitigated.
What it will never do
These are not defaults. They are boundaries, and they hold whatever a conversation asks for.
| Never | Why |
|---|---|
| Run a script, a SQL statement, or a shell command | This is the vector that would turn a prompt injection into code execution |
| Start, stop or cancel a job; activate a schedule | Making something run on its own, at night, is a person's decision |
| Delete an Onyx object | Nothing here removes anything |
| Return the value of a secret | list_secrets returns keys. A key is a name someone chose; it is not a secret |
| Write a connection string, or a variable's SQL query | Both are code that Onyx would later execute |
| Call an LLM | An MCP server talks to a host, not to a model |
| Open a port | The transport is stdio. There is no listening socket, no OAuth, no token issuance |
| Keep state between sessions | No database, no cache, no file |
Anything this server creates arrives inert: a new script is neither scheduled nor wired into a workflow, a new form is not published, a new schedule is not active. A person makes it live, in Onyx.
Install
The MCP host starts the server as a subprocess and speaks JSON-RPC over stdin/stdout. You never start it yourself, and nothing listens on a port.
With uv, nothing needs installing at all — uvx fetches the
package on demand:
uvx --from ads-onyx-mcp onyx-mcp-server --check
Or install it into an environment:
pip install ads-onyx-mcp
onyx-mcp-server --check
Python 3.11 or later.
Configure
Four settings, from the environment:
| Variable | Role |
|---|---|
ONYX_API_URL |
Root of the Onyx API — not the Angular front end, see below |
ONYX_TENANT_ID |
Numeric tenant id — the recommended path |
ONYX_USER |
Username or email |
ONYX_PASSWORD |
Password |
Optional: ONYX_TENANT (tenant name, resolved at startup, instead of the id),
ONYX_REQUEST_TIMEOUT_S, ONYX_VERIFY_TLS, ONYX_MAX_PAGE_SIZE, ONYX_LOG_LEVEL,
ONYX_ENV_FILE.
ONYX_ENV_FILE points at a .env file to read on startup. It is how you keep the password out of
your host's configuration file, which is plain JSON on disk. See
.env.example for the shape.
⚠️ ONYX_API_URL is the API, not the front end
In a typical Onyx deployment the Angular front end and the API live on two different hosts.
Pointing ONYX_API_URL at the front end reaches a different Onyx instance: it answers, it
authenticates, and it issues a perfectly valid token — for another tenant, or for the host. Nothing
in the response says so.
The server guards against this: after authenticating it checks that the token actually carries the tenant that was asked for, and refuses to start otherwise, naming the confusion. Better not to get there: the URL is the one your API client consumes, not the one you open in a browser.
⚠️ Prefer the numeric tenant id
Abp.TenantId expects an integer. The server also accepts ONYX_TENANT, the tenant name, which
it resolves at startup through Account/IsTenantAvailable — but that discovery is not always
available.
Since tenant ids are incrementing integers, an instance may deliberately disable discovery to stop them being enumerated. On such an instance, resolving a name yields a wrong id. ABP then discards the non-existent tenant and falls back to the host, and the symptom misleads:
- authentication succeeds on a same-named host account — with host permissions, therefore
without the
MultiTenancySides.Tenantpermissions thatPages.OConnectionsandPages.OJobInstancesare; - or it fails on a perfectly valid tenant account, with "Invalid user name or password".
Nothing in the response points at the cause. onyx-mcp-server --check does, by decoding the token
it obtained: a host token carries no tenant claim.
Verify before wiring anything up
onyx-mcp-server --check
This does not speak MCP. It resolves the tenant, authenticates, reads the instance version and the account's permissions, then calls two tools and shows a sample. It exists to separate two questions that are otherwise debugged together: do my Onyx credentials and rights work? and is my host configuration right?
# les valeurs ci-dessous sont un exemple
[1/7] Identifiant de tenant (fourni par configuration)
OK - identifiant 7, sans appel de decouverte
[2/7] Authentification (TokenAuth/Authenticate)
OK - jeton d'acces obtenu
[3/7] Identite portee par le jeton (revendications JWT)
utilisateur : mcp.lecteur (id 42)
tenant : 7 - conforme au tenant demande
[4/7] Version de l'instance (Session/GetCurrentLoginInformations)
version Onyx : 10.2.0
[5/7] Permissions accordées au compte (AbpUserConfiguration/GetAll)
OK - 104 permission(s) accordée(s)
[6/7] Tool list_connections (permission Pages.OConnections)
OK - 19 connexion(s) visible(s)
[7/7] Tool get_run_history (permission Pages.OJobInstances)
OK - 12480 exécution(s) visible(s)
Each step fails by naming its cause. Step 3 decodes the token, the only reliable judge of which tenant was actually applied. Step 5 separates "I lack the right" from "the configuration is wrong". Steps 6 and 7 are attempted independently, so a refusal on one says nothing about the other.
The password is never printed, nor is the token: the output can be pasted into a ticket as is.
⚠️ Two-factor authentication and reCAPTCHA
If the tenant enforces 2FA, or reCAPTCHA is active on login, password authentication cannot complete: a subprocess has nobody to ask for a code. Onyx signals these cases with an HTTP 200 carrying no token, which the server detects and reports plainly rather than failing opaquely. The account needs 2FA disabled, or an API-key mechanism on the Onyx side.
Wire it into a host
Claude Code
.mcp.json, in your project:
{
"mcpServers": {
"onyx": {
"command": "uvx",
"args": ["--from", "ads-onyx-mcp", "onyx-mcp-server"],
"env": { "ONYX_ENV_FILE": "C:/Users/you/onyx/.env" }
}
}
}
Restart Claude Code, then type /mcp: onyx should appear with its tools. Then ask a question in
plain language — there is no tool to pick and no API call to write:
— Which Oracle connections are configured?
— What failed last night, and how long did it run?
— This script references
{{dwh_pwd}}. Does that secret exist?
Claude Desktop (Windows)
Same server, same transport. The file is %APPDATA%\Claude\claude_desktop_config.json, and it only
exists after the application has run once.
Three differences, all specific to the desktop app:
- Use an absolute path to the executable. Claude Desktop does not inherit the shell's
PATH, so"command": "uvx"fails silently. Find it withwhere uvx, and use forward slashes or double the backslashes — it is JSON. - Quit the application completely, from the notification-area icon, not just close the window. Otherwise the configuration is not re-read.
- Logs are in
%APPDATA%\Claude\logs\—mcp-server-onyx.logfor the server's stderr,mcp.logfor the dialogue. A missing setting is reported there.
Cursor
.cursor/mcp.json in the project, same structure as Claude Code's .mcp.json.
One server per host — there is nothing to start
A common question: can I start the server in VS Code and use it from Claude Desktop? No, and there is no need to want to.
Over stdio, an MCP server is not a shared service. It opens no port and listens to nothing: its input and output are pipes owned by the process that spawned it. A second host has no address to connect to.
What happens in practice is simpler than the question assumes. You never start the server — the host spawns it on first need and kills it at the end of the session. Each host runs its own copy, each with its own token and no shared state, so "one process = one user = one tenant" stays true. And there is no conflict between them, because nothing is kept between sessions.
The tools
Tool descriptions and schemas are in English: that is what the model reads, and it reasons better on them. The project's own documentation is in French.
Twenty-three tools. Sixteen only read; seven write, and each states what it refuses.
Finding your way around
| Tool | Role |
|---|---|
list_objects |
Inventory by type, with ids. The entry point — every other tool takes a GUID |
search_objects |
Search by name, or by what an object's documentation says |
list_connections |
Sources and targets, technology, writable or not. Never a credential |
get_run_history |
Statuses, start and end times, computed durations, rows written |
get_workflow |
What a workflow runs, when, and who is told when it breaks |
check_hidden_failures |
Steps that fail without failing their workflow, across the instance |
get_documentation |
An object's documentation, as fields plus prose |
Reading what will run
| Tool | Role |
|---|---|
get_script |
A script's source, Python or shell, with credentials masked, plus its environment |
list_secrets |
Which secret keys exist. No value, ever |
list_project_variables |
Project variables, with their values — the middle link when a {{name}} resolves |
Writing
| Tool | Writes |
|---|---|
set_documentation |
An object's documentation. Requires the revision from get_documentation |
create_table_report |
A report and its widget. Accepts no SQL — it generates its own from the connection's catalogue |
add_widget_filter |
A filter on an existing widget, with an empty-safe, dialect-correct predicate |
create_form |
A form definition. Does not publish it: activation stays a click in Onyx |
create_form_report |
A report showing an existing, already-active form |
create_pipeline |
A source-to-target transfer. Never starts it, and never defaults the destructive action |
create_python_script |
A Python script, statically reviewed, inert |
create_shell_script |
A shell script that invokes repository code — the command is generated, not supplied |
update_python_script |
Replaces a Python body, after you have read it |
set_script_environment |
A script's environment variables, where credentials belong as {{references}} |
create_schedule / update_schedule |
A cron formula. Cannot activate it |
set_project_variable |
A project variable's value |
A few of these deserve a note.
list_objects was added after a measurement: on an instance with 157 documented objects, 112
were unreachable, because get_run_history was the only source of ids and reports and projects
have no run history at all.
check_hidden_failures finds what no screen shows. Onyx lets a workflow step be configured not
to interrupt its workflow when it fails; the workflow then finishes as a success and the failure
appears nowhere — not in the history, not in the error notification, since that is tied to the
workflow failing, which it does not. On a real instance this hid two scripts that had failed on
every run for three months. The tool also reports alerts_that_can_never_fire: workflows with an
error notification where every active step is non-blocking. They look monitored and are not.
get_script masks anything that looks like a credential before answering, and the mask is what
forces the fix: update_python_script refuses a body that still carries one, so correcting a script
means putting {{a_secret_name}} there instead. On the reference instance, that is how seven Python
scripts out of seventeen turned out to hold a credential in the clear.
list_secrets exists because a {{key}} that does not exist raises nothing: Onyx replaces it
with the empty string, silently, and the script runs with a missing value looking as though it
worked. It also counts the secrets whose hidden box is unticked — Onyx hands those values out in
cleartext to anyone with the secrets permission, and ticking the box costs nothing.
set_project_variable refuses a variable backed by a SQL query, and the reason is worth
knowing: Onyx returns a variable's value without running its query whenever the value is not empty.
Setting a value on such a variable would not sit alongside the query — it would retire it, for good,
with no error and no trace.
Safety
safety/ is not decorative. It mitigates the two real risks of this project: indirect prompt
injection, and exfiltration.
Some of the text flowing through this server is written by customer data: database driver
error messages, table names, extracts of failing rows. A run's message field is the direct
example. That text is treated as untrusted by principle — patterns imitating the structure of a
conversation are neutralised, invisible characters and ANSI sequences are stripped, and untrusted
content is labelled as such in the response so the model knows it is reading data, not
instructions.
This is not complete neutralisation. No pattern list is exhaustive, and claiming otherwise would be the real fault. The labelling is the part that matters most.
Any truncation is announced, with what is needed to ask for the rest. A partial answer the model believes is complete produces confident, wrong conclusions — "there are only three pipelines".
Static review of a script is, likewise, a text reading. It catches what is written in the clear, not what is hidden. It is not a sandbox and the tools say so in their own words rather than letting a reader assume otherwise.
Development
uv sync
uv run pytest # 727 tests, no network access
The tests never touch the network: the client is wired to an httpx.MockTransport and every Onyx
response comes from a fixture. That is a structural property, not a convention — there is no
socket.
src/onyx_mcp/
├── __main__.py the ONLY file that knows about the transport (stdio)
├── server.py server assembly and tool declarations
├── config.py environment variables → typed, validated settings
├── errors.py errors written to be read by a model
├── onyx/ Onyx API client: auth, ABP envelope, endpoints
├── safety/ sanitising, pagination, static review of Python
└── tools/ tool implementations, with no dependency on the protocol
__main__.py is the only file that knows about the transport. The day an HTTP transport is
needed, an entry point is added beside it; server.py, tools/, onyx/ and safety/ do not move.
Two tests keep that isolation from eroding.
API stability
The public contract of this package is twofold: the list of MCP tools and the environment variables. That is all.
The Python modules stay importable, but their API is internal and offers no stability guarantee: signatures, names and structure may change without a major version. Do not write code that depends on them.
Licence
Proprietary — use is reserved to holders of an Ads.Onyx licence from Alchimie Data Solutions. See LICENSE. Questions, and requests for other terms: contact@alchimiedatasolutions.com.
Release files for ads-onyx-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ads_onyx_mcp-0.1.0.tar.gz | 202.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ads_onyx_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 426.5 kB
Release files / ads_onyx_mcp-0.1.0.tar.gz
| Download URL | ads_onyx_mcp-0.1.0.tar.gz |
|---|---|
| Size | 202.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
20a8b8f9bf96d3d161d7419b05d2b7801e54ac45733c807e55d1b605f4ad3707
|
|
BLAKE2b-256 checksum How to use checksums |
6f539971c098bf70779e70cfb4f5029d5e35e6c59e870f1d6973ae522d41876c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / ads_onyx_mcp-0.1.0-py3-none-any.whl
| Download URL | ads_onyx_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 224.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
68401455775b05c21f5079f326f382c77199cb0bc17dbe11fc7c7df1b7d6ad90
|
|
BLAKE2b-256 checksum How to use checksums |
f8aac5185e153866944a56a959cf087576f3d6d982b9d1c6eacb97fa9e122334
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|