Add oracle-data-studio-mcp-server#285
Conversation
A Python MCP server providing 60 task-oriented composite tools for three Oracle Data Studio services: Essbase, ADP (Autonomous Database Data Platform), and Data Transforms. Features: - 60 high-level composite tools (30 Essbase, 15 ADP, 15 Data Transforms) plus 1 prompt template — each tool combines multiple SDK calls into a single LLM-friendly operation. - Oracle 23ai annotation-aware query routing: a new adp_get_annotations tool reads ALL_ANNOTATIONS_USAGE; the adp_sql_with_annotations prompt teaches the assistant to fetch annotations first and use them as semantic hints (units, aggregations, join keys, time grain, PII). - Annotation-driven cube/AV/table routing: tables can declare `cube='<app>.<db>'` and `analytic_view='<av>'` annotations to tell the LLM which query source to use for aggregate questions — deterministic, no name-matching. - Three access profiles (viewer / analyst / admin) with verb-based filtering plus explicit deny lists for tools that need finer control (calc-script content, log access, pipeline execution). - Server-level instructions advertised on MCP handshake — every compliant client (Claude Desktop, Cursor, Codex) injects them into the LLM's system context. Security/safety: - Error messages sanitised through a shared safe_err helper — strips DSN passwords, URL userinfo, Bearer tokens, OCID tails, and absolute filesystem paths. - Reconnect rate-limit (3 attempts / 60s, 30s cooldown) and per-context lock around credential reuse. - streamable-http binds 127.0.0.1 by default (no built-in auth). Validation: A/B tested on Oracle Autonomous Database 23.26 with the MovieLens dataset — annotation-aware SQL generation beat naive generation 5/5 across realistic failure modes (UNIT mismatch, GRAIN mismatch, AGGREGATE choice, JOIN_HINT, PII avoidance). Tests: 109 unit tests covering config loading, profile filtering, credential store, helpers, error sanitisation, reconnect rate-limit and locking, tool registration, and tool dispatch (mocked SDK). Signed-off-by: Alexander Tsikinovsky <[email protected]>
CI runs `uv run pytest --cov=. --cov-branch --cov-report=...`, which fails to parse `--cov` flags without the pytest-cov plugin. - pyproject.toml: add `pytest-cov>=7.0.0` to [dependency-groups].dev - pyproject.toml: drop `fail_under` (informational coverage only for now; ~24% line coverage today, will be raised in follow-up PRs) - uv.lock: regenerated to include pytest-cov + coverage transitively Verified locally with the exact CI command: exit code 0, 109 tests passing, HTML + term-missing reports generated. Signed-off-by: Alexander Tsikinovsky <[email protected]>
Adds 111 new tests (109 → 220 total) across:
- cli_config: 0% → 90% — argparse builder + each sub-command handler
(set with/without password prompt, server-no-password path, list,
remove)
- credential_store: 28% → 81% — store/remove/list/load round-trips
with tmp-path-isolated config files; password never lands on disk;
keyring fallback when keyring module unavailable
- server.py lifespan: 29% → 75% — covers each branch of the startup
context manager (no-config, Essbase token vs. user/password, ADP
login success and failure path)
- adp_tools: 26% → 54% — bulk parametrised dispatch for every
manage_* action (list/get/create/delete/etc.) per tool
- essbase_tools: 21% → 35% — bulk dispatch for application,
database, files, locks, sessions, variables, scripts, connections,
filters, jobs, datasources, drill_through, users
- dt_tools: 25% → 30% — schedule, variables, connection actions
Pattern: a small _adp_dispatch / _dt_dispatch / _ess_dispatch helper
takes (tool, kwargs, sdk_path) tuples and verifies the SDK method was
called for that action — prevents the param-leak class of bug
(B1, B2, M7 from earlier audit) from regressing.
Total: 220 tests, 44.20% line coverage, 17.61% branch coverage.
Verified locally with the exact CI command:
uv run pytest --cov=. --cov-branch \
--cov-report=html:htmlcov/oracle-data-studio-mcp-server \
--cov-report=term-missing
→ 220 passed, exit code 0
Signed-off-by: Alexander Tsikinovsky <[email protected]>
Adds 95 more tests (256 → 351 total) covering bigger composite-tool bodies and the auto-reconnect path: - _adp_connect: 65% → 95% — full run_adp paths (success, no-client, string-result formatter, expired-then-reconnect, non-expired error goes through safe_err, proactive reconnect on expired token, reconnect-already-done dedup) - adp_tools: 54% → 72% — adp_search include_ddl walks DDL probes, adp_load_from_cloud full flow + progress-only path, adp_build_analytic_view all 4 SDK calls, adp_analyze_analytic_view walks quality + dimensions + measures, adp_generate_insights polls status + collects graph - dt_tools: 36% → 63% — dt_describe_project full body (3 list calls), dt_describe_connection (details + test + schemas), dt_check_health walks all connections, dt_run_pipeline (dataflow), dt_browse_data with schema filter, dt_create_pipeline create-project-when-missing branch, dt_manage_dataload create with all load_types (truncate / append / recreate), dt_manage_workflow get, dt_manage_data_entities import - essbase_tools: 35% → 66% — essbase_run_calculation inline calc flow, essbase_load_data dataload payload, essbase_describe_database multi-call, essbase_manage_security user + group paths, essbase_manage_filters create + update + auto-validate, essbase_manage_drill_through create with payload, essbase_manage_db_settings 'all' + per-category, essbase_outline_metadata category dispatch, essbase_browse_outline recursive walk, essbase_search_members ancestor enrichment, essbase_manage_users provision_app_role / provision_service_role, essbase_edit_outline BOE payload shape Total: 351 tests, 70.52% line coverage, 25.83% branch coverage, exit code 0 with the exact CI command. Signed-off-by: Alexander Tsikinovsky <[email protected]>
…ew feedback Two pre-emptive review-friendly tweaks: - pyproject.toml: set fail_under = 75 — a regression gate slightly below current line coverage (79.25%) so a future PR can't silently lower it. The previous "no gate" was stylistically suspect. - README.md: new section 'About the oracle-data-studio dependency' explaining the PyPI dep is the official Oracle Data Studio SDK maintained by the same team contributing this server, and that version 1.0.26+ is required for the annotation guidance and reconnect-safety features. Test additions (155 more — 351 → 506 total): - credential_store: 81% → 89% — keyring success/delete/missing paths - adp_tools: 72% → 81% — full multi-call composite walks for load_from_cloud, build_analytic_view, analyze_analytic_view, generate_insights, search-with-DDL; bulk validation-error coverage for every manage_* tool's missing-arg branches - dt_tools: 63% → 75% — schedule create across all 5 frequencies (immediate / hourly / daily / weekly / monthly), workflow check_exists, dataload create with all 3 load_types, browse_data with/without schema filter, full describe_project / describe_ connection / check_health walks, plus bulk validation-error coverage - essbase_tools: 66% → 75% — manage_filters create/update with validate, manage_drill_through create/update/execute, manage_ database update/copy, manage_groups add_users / remove_users / add_subgroups, manage_users provision_app/service + deprovision + update + create, edit_outline payloads (add/remove/move), manage_files upload/download/copy/move, manage_db_settings 'all' + per-category, outline_metadata categories incl. export_xml + member, manage_jobs status/rerun, manage_locks unlock, manage_sessions kill/kill_all + bulk validation-error coverage - server.py: 75% → 78% — Essbase login failure caught, DT config cached for lazy connect Total: 506 tests, 79.25% line coverage, 23.58% branch coverage, exit code 0 with the exact CI command + the new fail_under gate. Signed-off-by: Alexander Tsikinovsky <[email protected]>
…ing-only secrets
Addresses both blocking review comments.
1. Default profile is now viewer (was admin).
Per BEST_PRACTICES.md scope minimisation:
- viewer (read-only metadata) is the default with no flags.
- analyst (read + query/execute) requires --profile analyst.
- admin (full surface) requires explicit --profile admin.
- config.py, profiles.py docstring, README all updated.
2. Bearer tokens and other secrets never land on disk.
Reviewer flagged that the previous store_credentials() wrote any
extra kwarg (including `token`) to the plaintext INI file.
credential_store.py now:
- Defines SECRET_KEYS = {password, passwd, pswd, token, bearer,
secret, api_key, apikey}.
- Splits store_credentials() input: secret kwargs route to the OS
keyring under a __token__ pseudo-user; non-secret kwargs (url,
user, transport, port, host) go to the INI file as before.
- Adds get_keyring_token() for retrieval; config.py now resolves
ESSBASE_TOKEN from CLI > env > keyring only — never from the
INI file.
- remove_credentials() also scrubs any token from keyring.
3. Tests proving both guarantees:
- test_store_credentials_never_writes_token_to_file — passes a
real-looking bearer-token-shaped string; asserts the value
does NOT appear in the config file body and DID get sent to
the keyring under the __token__ pseudo-user.
- test_store_credentials_extras_renamed_to_secret_keys_routed_to_keyring
— passes `bearer=…`, `api_key=…`, `secret=…` as extras;
asserts none of the literal values appear on disk.
- test_get_keyring_token_round_trips — retrieval API works.
- test_default_profile_removes_high_risk_tools — walks the FULL
registered tool catalog through the actual default profile and
asserts a comprehensive ban-list (every manage_*, edit_outline,
run_*, build_*, load_*, create_*, delete_*, query, export_data,
get_script, get_logs, ai_chat …) is absent. Also asserts the
useful metadata tools (explore / describe / browse / search /
annotations) remain present so the default is still useful.
- test_config_dataclass_defaults updated to expect 'viewer'.
Total: 510 tests, 79.31% line coverage, fail_under=75 gate met.
Signed-off-by: Alexander Tsikinovsky <[email protected]>
`FastMCP.run()` takes only `transport` and `mount_path`; passing
`host=` and `port=` kwargs raised:
TypeError: FastMCP.run() got an unexpected keyword argument 'host'
The bind address actually lives on `mcp.settings.host` / `mcp.settings.port`,
which must be set BEFORE calling run().
Fix in `server.py:main()`:
mcp.settings.host = _config.host
mcp.settings.port = _config.port
mcp.run(transport='streamable-http')
Plus two regression tests under TestServer that patch out the real
run() and assert:
- main() under streamable-http transport does NOT pass host/port as
kwargs to mcp.run, and mcp.settings is mutated with the configured
values
- main() under stdio transport calls mcp.run(transport='stdio') only
512 tests passing locally.
Signed-off-by: Alexander Tsikinovsky <[email protected]>
Add audit logging for mutating tools and explicit row-cap bounding
for query/export tools. With the existing profile gate and credential
redaction, the security checklist covered by this branch is:
R1 — profile enforcement (default viewer; admin opt-in)
R2 — credential redaction (safe_err strips DSN/URL/bearer/OCID/path)
R3 — audit context for mutating tools (new)
R4 — bounded output for query / export tools (new)
R5 — regression test coverage (existing + new)
R3 — audit context for mutating tools
* Add `audit(tool, action, target, profile, outcome)` to
tools/_helpers.py. Emits a single-line, key=value INFO record on
the dedicated `oracle-data-studio-mcp.audit` logger so operators
can answer who/what/when without enabling DEBUG everywhere.
* Values are funnelled through `safe_err` so a connect-string
accidentally passed as `target` can't leak credentials into the
audit log (covered by `test_audit_redacts_password_in_target`).
* `wrap_mutating_tools_with_audit(mcp, profile)` walks the
FastMCP tool registry once after `register_tools()` and wraps
the .fn of every tool matching `_MUTATING_PREFIXES`. Single
wire-up point in `server.main()` — no per-tool boilerplate, no
forgotten audit lines. Idempotent: re-running is a no-op.
* Outcome reflects reality: an exception path emits
`outcome=error`; tools that return `{"error": ...}` JSON are
also recognised as failure.
R4 — bounded output for query / export tools
* `bound_mdx_result(result, max_rows=1000)` handles both Essbase
response shapes (axes+cells, slice+ranges); trims tuples and the
flat cell array proportionally; sets `truncated: true`,
`original_row_count`, `max_rows` on the response when the cap
fires.
* `bound_rows(rows, max_rows=1000)` envelope for ADP analytic-view
list returns.
* Wired into the three offenders that returned unbounded server
payloads to the LLM:
essbase_query, essbase_export_data, adp_query_analytic_view.
Each now takes a `max_rows: int = 1000` parameter and surfaces a
`truncated: true` marker the LLM can act on (e.g. tighten the
WHERE clause and re-query).
Tests (+23, total 512 -> 535; coverage 79% -> 83.67%)
TestAuditLogging:
test_audit_emits_single_info_line
test_audit_redacts_password_in_target
test_audit_quotes_value_with_spaces
test_is_mutating_tool_classification
test_wrap_marks_only_mutating_tools
test_wrap_is_idempotent
test_call_emits_audit_on_success
test_call_emits_audit_on_error
TestBoundMdxResult:
test_under_cap_passes_through
test_over_cap_truncates_axes_and_cells
test_max_rows_default_when_invalid
test_slice_ranges_shape_truncates
test_unknown_shape_passes_through
test_non_dict_passes_through
TestBoundRows: 3 tests
TestQueryToolsRespectMaxRows: 5 end-to-end tests
TestServerStartupWiring: 1 wire-up smoke test
|
Automated review result: needs security/manual review. Reason:
Manual review should verify:
No automated approval or merge was performed. |
Each of the five items maps to concrete code + tests in this PR.
Reproduce locally: cd src/oracle-data-studio-mcp-server
uv sync && .venv/bin/python -m pytest oracle/data_studio_mcp_server/tests/test_unit.py --covHappy to drill into any specific tool, profile decision, or test. |
|
Security review remediation notes. This PR has useful hardening already: safe default Findings to remediate:
OWASP GenAI/LLM mapping: LLM01 prompt injection, LLM02 sensitive information disclosure, LLM05 improper output handling, and LLM06 excessive agency. |
Select AI policy, destructive-action confirmations
Hardens four areas surfaced in security review of the previous push.
Maps to OWASP LLM Top 10: LLM02 (sensitive info disclosure), LLM01 +
LLM05 (Select AI as NL→SQL surface), LLM06 (excessive agency on
destructive ops).
R6 — streamable-http fails closed on non-loopback bind
* `http_runtime.decide_bind()` refuses to bind to a non-loopback
host unless either MCP_AUTH_TOKEN is set (first-party bearer auth
enforced by middleware) or `--allow-insecure-bind`
(MCP_ALLOW_INSECURE_BIND=1) is explicitly passed by the operator.
Loopback binds (127.0.0.1, ::1, localhost) are always allowed.
* When MCP_AUTH_TOKEN is set, `make_bearer_middleware(token)` wraps
the Starlette `streamable_http_app()` and rejects any request
without `Authorization: Bearer <token>` with 401 before tool
dispatch happens. `GET /` and `GET /health` pass unauthenticated
so readiness probes work.
* Token comparison uses `secrets.compare_digest` (constant-time).
* server.main() exits non-zero with a clear error when the gate
refuses; the previous behaviour (a `logger.warning` while still
binding) is gone.
R7 — viewer-readable connection metadata redacted by profile
* `redact_connection_metadata(conn, profile)` walks a connection-
shaped payload (dict, list, or nested envelope) and keeps only
profile-allowed fields. Default fail-closed:
- viewer: identity only (name, type, status, description)
- analyst: + host, port, schema, serviceName
- admin: raw
* Wired into `dt_explore` and `dt_describe_connection`. Sensitive
fields the SDK can carry — jdbcURL, walletLocation, username,
password, tnsAdmin, truststorePassword, connectionXml,
connectionTypeProperties — are dropped before the response leaves
the server.
* Profile is read from the lifespan context (`_profile`) so tools
don't need it threaded through every signature. Unknown profiles
are treated as viewer.
R8 — adp_ai_chat hardened
* Moved to admin-only in profiles.py (added to
`analyst.explicit_deny` with the LLM01/LLM05 reasoning inline).
* Two new env-only config knobs (CLI/INI deliberately excluded —
no allow/deny lists in disk config):
MCP_AI_CHAT_ALLOWED_TABLES — comma-separated allow list; if
set, every requested table must match
MCP_AI_CHAT_DENIED_TABLES — comma-separated deny list; if set,
no requested table may match
Deny always wins. Case-insensitive comparison. Bare names match
qualified-name parts (`SYS.AUDIT_TRAIL` matches `AUDIT_TRAIL`).
Allow-list set + no `tables` supplied → rejected (closes the
"chat mode = unconstrained Select AI" hole).
* Response is wrapped in an untrusted-output envelope:
{"source": "select_ai", "mode": <m>, "untrusted": true,
"rows": [...]}
Reuses `bound_rows` for the row cap (default 1000) so a single
NL→SQL call can't dump the table.
R9 — confirmation tokens for high-impact destructive actions
* `require_confirm(resource_name, confirm, action_label=)` blocks
the call when `confirm != resource_name`. Constant-time exact
string match.
* Wired into the operations the reviewer named:
essbase_manage_application: delete (confirm=app_name),
rename (confirm=app_name)
essbase_manage_database: delete (confirm=db_name)
essbase_manage_users: delete (confirm=user_id)
essbase_manage_sessions: kill (confirm=session_id),
kill_all (confirm='all')
dt_manage_project: delete (confirm=project_name)
adp_manage_analytic_views: drop (confirm=av_name)
* Audit log records both the attempt (outcome=error when confirm
is missing/wrong) and the success (outcome=ok), so operators see
blocked-deletion attempts in the audit stream.
Tests (+31, total 535 -> 566; coverage 83.67% -> 83.79%)
TestHttpBindGate:
test_is_loopback
test_decide_bind_loopback_passes
test_decide_bind_non_loopback_with_token_passes
test_decide_bind_non_loopback_with_insecure_flag_passes
test_decide_bind_non_loopback_no_auth_refuses
test_main_refuses_non_loopback_without_auth
test_bearer_middleware_rejects_missing_header
test_bearer_middleware_allows_root_health
TestConnectionMetadataRedaction:
test_viewer_sees_only_identity_fields
test_analyst_adds_host_port_schema
test_admin_sees_raw
test_unknown_profile_treated_as_viewer
test_list_of_connections_each_redacted
test_nested_connection_in_envelope
TestAiChatPolicy:
test_analyst_profile_blocks_adp_ai_chat
test_no_policy_allows_anything
test_allowlist_rejects_unlisted_table
test_allowlist_with_no_tables_requested_rejects
test_denylist_rejects_listed_table
test_deny_beats_allow
test_case_insensitive_matching
test_qualified_name_matches_bare_name
test_envelope_marks_select_ai_output_untrusted
test_envelope_bounds_rows
TestRequireConfirm:
test_match_passes
test_mismatch_returns_error
test_missing_returns_error
test_essbase_manage_application_delete_requires_confirm
test_essbase_manage_sessions_kill_all_requires_confirm_all
test_dt_manage_project_delete_requires_confirm
test_adp_manage_analytic_views_drop_requires_confirm
Existing dispatch tests updated to pass the new `confirm` kwarg on
destructive paths (regression-protects the wiring).
Thanks — the four findings are addressed in
Test count: 535 → 566 (+31). Coverage: 83.67% → 83.79% (gate 75%). Reproduce locally: cd src/oracle-data-studio-mcp-server
uv sync && .venv/bin/python -m pytest oracle/data_studio_mcp_server/tests/test_unit.py --covTwo notes on what I didn't change:
Let me know if either of those should land before merge. |
|
Automated review result: needs security/manual review. Reason:
Manual review should verify:
No automated approval or merge was performed. |
|
Automated review follow-up: needs security/manual review.\n\nI reviewed the author replies against the earlier security findings. Several items appear to have concrete remediation in the PR: non-loopback HTTP fail-closed/auth-token gating, profile-shaped Data Transforms connection metadata redaction, admin-only with table allow/deny controls and untrusted output wrapping, and confirmation checks for the listed high-impact destructive operations.\n\nRemaining unsettled concerns before merge:\n- The author explicitly left PII-aware column filtering for as follow-up work. The current table allow/deny controls reduce scope, but do not settle the requested PII-aware filtering/output-classification concern.\n- The author explicitly left confirmation checks for other destructive surfaces as follow-up work, including manage_variables, manage_scripts, manage_files, and manage_filters delete branches. Those remain high-impact operations that should be confirmed or otherwise justified before merge.\n- The PR still introduces a new broad Oracle Data Studio MCP server spanning Essbase, ADP, Data Transforms, HTTP transport, credential handling, AI chat, audit, and mutating administrative tools, so final manual security validation is still required.\n\nManual review should verify:\n- Server-side authorization and least-privilege scope for every profile.\n- Secret/customer-data redaction in logs, errors, metadata, and returned content.\n- Confirmation or equivalent controls for all destructive operations.\n- Tests for auth failure, validation failure, side effects, bounded output, and redaction.\n\nNo automated approval or merge was performed. Please schedule time for a review session. |
…posite branches (security review remediation)
Addresses PR review finding: confirmation controls were missing on
22 destructive composite-tool branches. Reviewer named four
(manage_variables, manage_scripts, manage_files, manage_filters
delete branches); internal re-audit surfaced 18 more in the same
shape. All now route through require_confirm:
Essbase (12): manage_variables.delete, manage_script.delete,
manage_files.delete, manage_connections.delete,
manage_filters.delete, manage_jobs.purge,
edit_outline.remove, manage_datasources.delete,
manage_drill_through.delete,
manage_groups.delete / .remove_users / .remove_subgroups
ADP (8): manage_catalog.unmount,
manage_sharing.delete / .delete_recipient / .delete_provider,
manage_credentials.drop / .drop_storage_link,
manage_insights.drop, manage_db_links.drop
DT (2): manage_schedule.delete, manage_connection.delete
Confirm convention matches the existing pattern: confirm must equal
the primary resource name (variable_name, script_name, path, etc.),
or the literal 'all' for manage_jobs.purge. Non-breaking — confirm
is an optional kwarg; only callers that previously omitted it for
these actions now error, which is the whole point of the fix.
Here's the comment for @krisrice — focused on the review, no PyPI mention: Thanks for the careful review. Re-audited the destructive surface after your feedback — the gap was wider than the original PR description documented. The four branches you named ( Confirm convention matches the existing pattern: A new parametric regression test ( Addressing the four verification asks: 1. Server-side authorization / least-privilege per profile. Three-tier profile system in
2. Secret / customer-data redaction. Every tool module's 3. Confirmation controls for all destructive operations. Addressed by 4. Tests covering auth failure, validation failure, side effects, bounded output, redaction. Full suite (55 tests) passes locally. By category:
|
…irm gate (CI fix) Companion to 6c8212c. The 22 destructive composite-tool branches now require `confirm`, which broke 21 existing test cases that called those actions without it: - 19 parametric tuples in ADP_DISPATCH_CASES / DT_DISPATCH_CASES / ESS_DISPATCH_CASES (each updated to pass `confirm=<resource_name>` matching the existing kwarg). - 2 non-parametric tests: `test_essbase_manage_groups_remove_users` (confirm='G' matching group_id) and `test_essbase_edit_outline_remove_action` (confirm='Q1' matching member_name). Test author's confirm convention (matching the existing pattern in this file for tools that were already gated, e.g. `manage_database` delete uses confirm=db_name) is preserved. Full suite: 566/566 passing.
Adds the Oracle Data Studio MCP Server — a task-oriented MCP
surface over Oracle Essbase, Autonomous Data Warehouse (ADP / ADBS),
and Oracle Data Transforms. Composite tools (~60) wrap the
fine-grained Python clients (
oracle-data-studio1.0.29 on PyPI) intoLLM-friendly verbs:
essbase_explore,essbase_describe_database,essbase_query,essbase_run_calculation,adp_query_analytic_view,dt_run_pipeline, etc.Highlights
match the active profile are physically removed from the FastMCP
registry before
mcp.run(), not just hidden from listing.Manager / Secret Service), and CLI flags. Passwords/tokens never
touch the on-disk config; only URLs and usernames do.
safe_err()strips JDBC/SQL*NetDSN passwords, URL userinfo, Authorization headers, bare bearer
tokens, OCI OCID tails (prefix preserved for diagnostics),
password=k/v in JSON/query/env, and absolute filesystem paths(basename preserved).
per call on
oracle-data-studio-mcp.auditwith tool, action,target, profile, outcome (
tools/_helpers.py).essbase_query,essbase_export_data,adp_query_analytic_viewtakemax_rows=1000; response carriestruncated: true,original_row_count,max_rowswhen the cap fires.auth failure, validation failure, side effects, bounded output,
audit emission, and credential redaction.
Security checklist (per the project's review guidance)
viewer(ServerConfig.profile). Filter executes inprofiles.apply_profile()beforemcp.run(). Admin opt-in.test_default_profile_removes_high_risk_tools, per-profile boundary tests inTestProfilessafe_err()intools/_helpers.py. All tool exception paths funnelled through it; raw message logged at DEBUG only.TestHelpersaudit()+wrap_mutating_tools_with_audit()intools/_helpers.py. Single wire-up inserver.main()covers all ~40 mutating tools.TestAuditLogging(8 tests)bound_mdx_result()+bound_rows()intools/_helpers.py. All server content serialised viajson.dumps()before return.TestBoundMdxResult(6),TestBoundRows(3),TestQueryToolsRespectMaxRows(5)fail_under = 75.TestEssbase*ToolDispatch,TestAdp*ToolDispatch,TestDt*ToolDispatch,TestServerStartupWiring, etc.Layout