Skip to main content

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.Tenant permissions that Pages.OConnections and Pages.OJobInstances are;
  • 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:

  1. Use an absolute path to the executable. Claude Desktop does not inherit the shell's PATH, so "command": "uvx" fails silently. Find it with where uvx, and use forward slashes or double the backslashes — it is JSON.
  2. Quit the application completely, from the notification-area icon, not just close the window. Otherwise the configuration is not re-read.
  3. Logs are in %APPDATA%\Claude\logs\mcp-server-onyx.log for the server's stderr, mcp.log for 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)

Source distribution for ads-onyx-mcp 0.1.0
File Size Uploaded
ads_onyx_mcp-0.1.0.tar.gz 202.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ads-onyx-mcp 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

0.3.0

2 release files

0.2.0

2 release files

This release

0.1.0 This release

2 release files

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