chore/fix: dependabot bumps + AI/MCP demo hardening + mondrian 4.8.1.9#875
Merged
Conversation
…, postgresql Picks up three of the four open dependabot bumps: - spring-core 6.2.8 → 6.2.11 (#842 — CVE patch releases) - spring-security 6.5.1 → 6.5.10 (#843 — security/bugfix line) - postgresql 42.7.4 → 42.7.11 (#841 — JDBC driver patch) Held back: h2 1.4.188 → 2.2.220 (#840). H2 v2 is a major-version jump with a stricter SQL dialect that breaks the bundled FoodMart seed under the launcher — the async-query IT failed with the new h2. Filed for follow-up; the FoodMart seed SQL needs regeneration against an h2-2.x baseline before we can take the bump. The four dependabot PRs failed CI under their own runs because the GH Packages auth token for mondrian-saiku isn't available in the Dependabot secrets context (only Actions secrets are wired). Tests verify the bumps locally: 93 unit tests + 78 integration tests pass with the three accepted bumps applied.
This was referenced May 16, 2026
The login-page panel was rendering two big, fully-expanded cards (AI Query
API + MCP) below the sign-in form. On a demo deployment that pushed the
demo credentials and the DXT download well below the fold, and visitors
read the open cards as "this is what I need to read before logging in"
rather than reference material.
Also restore the .install-row DXT download button that was promised but
never actually wired up in admin (the CSS shipped without the markup).
- ApiAccessAdmin: $props defaultOpen=false; wrap each <section class="card">
in <details class="card" open={defaultOpen}> with the <h3> in <summary>.
- admin/+page.svelte: open by default (operator-facing, screen real estate
isn't a constraint).
- LoginForm.svelte: leave the new default (closed); the existing scroll
fix stays so visitors can still expand and reach below-the-fold content.
- Reintroduce <a download="saiku.dxt"> button inside the MCP card so the
user can grab the Claude Desktop bundle from the admin console too —
matches the original "i don't see a link to the dxt in the admin page"
feedback.
Previously the runtime-generated /rest/saiku/info/mcp.dxt bundle wrote
`server.type: "remote"` with a `transport.url`. Claude Desktop's manifest
validator (current shipping versions) rejects that with:
Invalid enum value. Expected 'python' | 'node' | 'binary', received 'remote'
The "remote" type is in the published DXT spec but not yet recognised by
the validator on Claude Desktop's release channel, so installing the
bundle failed at the front door.
Switch to the node-shim shape that every current host understands:
server.type = "node"
server.entry_point = "server.js"
server.mcp_config = { command: "node", args: ["${__dirname}/server.js"] }
The shim itself (also generated, embedded in the zip) is one screen of
JavaScript that spawns `npx -y mcp-remote@latest <url>` — mcp-remote is
the canonical stdio↔streamable-http bridge and ships through npx so the
DXT doesn't have to bundle node_modules. The MCP URL is JSON-encoded
into the shim so embedded quotes / unicode survive into JS source.
InfoResource.java
- buildDxtManifest now produces the node-shim manifest.
- dxtShim(mcpUrl) returns the shim source (package-private for tests).
- zipManifest → zipBundle(manifest, shimJs): two-entry zip
(manifest.json + server.js).
- getMcpDxt threads the URL through to both helpers.
InfoResourceTest.java
- Existing manifest assertions updated for the node-shim shape.
- Bundle assertion now walks all entries (order-independent) and
checks both manifest.json + server.js are present.
- New dxtShim_jsonEncodesUrlAndSpawnsMcpRemote: confirms a URL with an
embedded quote round-trips through JSON encoding safely.
- mcpDxt_versionFallsBackTo000WhenUnset: scans entries for
manifest.json rather than assuming index 0.
The previous DXT shim spawned `npx -y mcp-remote@latest <url>`. That
worked under a regular Node install but not inside Claude Desktop's
built-in Node sandbox, where `npx` isn't on the PATH — the spawn failed
silently and the host reported only "Server disconnected" with no
useful diagnostics.
Replace the shim with a self-contained JSON-RPC bridge that uses only
Node stdlib (http, https, url, process.stdin/stdout). The bridge:
- Reads line-delimited JSON-RPC from stdin
- POSTs each line to the configured /mcp URL
- Captures `mcp-session-id` from the initialize response and threads
it on every subsequent request (streamable-http session contract)
- Forwards JSON responses or unwraps `data:` lines from SSE responses
- Writes nothing for 202 empty-body notification acks
End-to-end test against demo.saiku.bi confirms the shim now drives a
full handshake (initialize → notifications/initialized → tools/list)
and surfaces all 6 Saiku tools.
Test updates:
- dxtShim_jsonEncodesUrlAndBridgesStdioToHttp: asserts the bridge
reads stdin, writes stdout, and threads mcp-session-id (renamed
from the old SpawnsMcpRemote assertion).
- mcpDxt_returnsZipWithManifestAndShimWhenMcpConfigured: shim
assertions updated for the new bridge contract.
mcp-proxy validates the CallToolResult envelope through pydantic and
rejects bare arrays in structuredContent with:
1 validation error for CallToolResult
structuredContent
Input should be a valid dictionary
[type=dict_type, input_value=[...], input_type=list]
The MCP spec is on mcp-proxy's side here — structuredContent is defined
as an object. Two of our tools return arrays (list_cubes, search_members),
so before this fix Claude Desktop saw the validation error instead of
the cubes list.
Wrap any non-object body under {"items": ...} at the jsonResult layer
so all six tool entry points stay covered. The text content
(content[0].text) keeps the original unwrapped JSON so agents that read
content[0] directly see the array as-is — only the structuredContent
side gets the wrapper.
Test updates:
- jsonResultWrapsSuccessfulResponseAsStructuredContent: now asserts
structuredContent is an object with .items[], while content[0]
stays as the raw array.
- jsonResultPassesObjectBodiesThroughUnwrapped: new — verifies
describe_cube / run_query shaped responses still pass through
without a spurious items wrapper.
…uncate the rest (saiku#877)
`OlapAiCubeMetadataService.buildSchema` previously wrapped the entire
walk over `getAllDimensions → hierarchies → levels` in a single
outer try/catch at the dimension-collection boundary. A throw from any
inner step — `discoverService.getAllDimensionHierarchies`,
`getAllHierarchyLevels`, `dim.getHierarchies`, annotation parsing,
anywhere — escaped the inner loops, was swallowed by the outer catch,
and ended iteration. Every dimension that came after the broken one
was silently dropped.
On the demo we saw the Sales cube reduced to just the Time dimension
while `dimensionAliases` still referenced `customer` (because aliases
are populated on a separate code path). That looked like an enrichment
bug to a confused agent; it was actually a swallowed mid-iteration
throw triggered upstream by mondrian-saiku#30's Calcite-H2 introspection
flake.
Refactor:
- Extract `buildDimension(cube, dim)` / `buildHierarchy(cube, dim, h)` /
`buildLevel(cube, h, lvl)` helpers, each owning a tight scope.
- Wrap each scope's hot points (annotation parse, hierarchy fetch,
level fetch, the per-element body) in its own try/catch:
- per-level catch keeps siblings going if one level can't be built
- per-hierarchy catch keeps siblings going if one hierarchy
can't enumerate levels
- per-dimension catch keeps siblings going if one dimension can't
enumerate hierarchies
- The outer `getAllDimensions` call still has its own catch so a
failure there leaves an empty dimensions map rather than throwing
out of buildSchema (existing behaviour, preserved).
The fix is independent of mondrian-saiku#30 — even on a healthy Calcite
path, a single bad annotation or transient JDBC blip on one level
shouldn't wipe out the rest of the cube's schema. Reported errors are
logged at WARN with the (cube, dim, [hier, [level]]) tuple so deployments
can audit silently-skipped pieces.
Tests:
- buildSchema_brokenDimensionDoesNotAbortRemainingDimensions
- buildSchema_brokenHierarchyDoesNotAbortRemainingHierarchies
- buildSchema_brokenLevelDoesNotAbortRemainingLevels
Each injects a synthetic throw at the relevant scope and asserts the
sibling scopes still register. All three failed against the previous
code (e.g. "Product dim survives even though Broken was in between" —
it didn't) and pass after the refactor. Full suite: 405 tests, 0
failures.
…ver cycle
The shim cached its mcp-session-id on the first initialize response and
never updated it. When an operator restarted saiku-mcp-proxy (or the
saiku-demo container was recreated), the next client request still sent
the dead session id and the server replied with:
{"jsonrpc":"2.0","id":"server-error","error":{"code":-32600,"message":"Session not found"}}
Two things were wrong with that response from the client's perspective:
the id was the literal string "server-error" rather than the client's
request id, so Claude Desktop saw it as orphaned and ignored it; and
the shim had no way back to a working session, so every subsequent call
hit the same wall until the user manually toggled the connector off/on.
Shim changes:
- `if (sid && !sessionId) sessionId = sid` → `if (sid) sessionId = sid`.
Always honour the latest mcp-session-id the server hands us — covers
the initial handshake AND any reissue after server-side restart.
- When the client itself sends `initialize`, drop the cached session
id first. Forces a fresh server-side session and avoids "already
initialized" / stale-id collisions on Claude-driven reconnect.
- New `isSessionLoss(r)` heuristic: HTTP 404, HTTP 400 with /session/i
in the body, or a JSON-RPC `error.message` matching /session/i.
- New `rehandshake()`: nulls sessionId, sends a canned initialize +
notifications/initialized to refresh the server's view.
- New `handleLine` wrapper: if a non-initialize request comes back as
a session loss, re-handshake and replay the SAME line once. The
replay response carries the client's original id so the client
consumes it as the normal answer to its in-flight call.
End-to-end verification against demo.saiku.bi: drive the shim through
initialize + tools/call (success), then `systemctl restart
saiku-mcp-proxy` mid-session, then another tools/call. Pre-fix the
second tools/call returned id="server-error" / "Session not found".
Post-fix the shim logs "session lost — re-handshaking and replaying
tools/call" to stderr, the client sees id=3 with the full 6-cube
items list, and never knew there was a hiccup.
Tests:
- dxtShim_recoversFromMidSessionServerCycle: asserts the shim source
contains the session-loss detection + rehandshake markers and that
the gated `!sessionId` check was removed in favour of unconditional
update on every response carrying mcp-session-id.
…2 introspection)
End-to-end verified against demo.saiku.bi with Calcite as the default
backend (no -Dmondrian.backend=legacy):
* GET /rest/saiku/api/ai/schema/unknown_foodmart/FoodMart/FoodMart/Sales
now returns all 6 dimensions (customer, performance season day,
product, promotion, store, time). Pre-bump only `time` came back.
* POST /rest/saiku/api/ai/query against Sales x Store/Stores/Store Country
returns status SUCCESS / USA / 266,773 Unit Sales. Pre-bump it died
with `IllegalArgumentException: field [time_id] not found; input
fields are: []` from Calcite's RelBuilder.
Compat fix piggybacked on the bump: 4.8.1.9 retyped RolapUtil.SQL_LOGGER
and RolapUtil.LOGGER from org.apache.log4j.Logger to org.slf4j.Logger as
part of upstream's log4j-1.x drop. Saiku's in-tree override of
mondrian.rolap.SqlStatement (saiku-webapp, same package so it can reach
both fields directly) imported the log4j-1.x type for its own LOG field;
the moment the JVM resolved that against the new RolapUtil it threw
NoSuchFieldError("org.apache.log4j.Logger SQL_LOGGER") at first cube
build. Switching the import + the static LOG factory call over to slf4j
keeps the override binary-compatible with the new mondrian and aligns
with the rest of the stack (slf4j 2.0 + log4j2 1.2-api shim).
The demo container is back on Calcite default; the legacy backend
override (JAVA_TOOL_OPTIONS=-Dmondrian.backend=legacy) introduced as a
workaround is dropped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Originally just the three dependabot consolidations; the demo-stabilisation work from 2026-05-16 piled on top after a long debugging session against demo.saiku.bi. Cohesive enough to ship as one PR — every commit is independently verifiable end-to-end against the demo.
Summary
06b1bc2ba1b9dbfb18443e90server.type: "remote"(Claude Desktop validator rejected with "Expected python|node|binary") to thenode+ shim form60647f67npx mcp-remotespawn that failed because npx isn't on Claude's built-in-node PATHb50a14e3jsonResultwraps array tool bodies under{ items: [...] }so strict MCP hosts (mcp-proxy pydantic, Claude) don't rejectlist_cubes/search_memberswithdict_typevalidation errorsa2380cadOlapAiCubeMetadataService.buildSchemaso one broken level/hierarchy/dimension no longer truncates the rest (closes #877)a4aea53c784ce1b7mondrian.rolap.SqlStatementoverride for 4.8.1.9's log4j-1.x dropVerified end-to-end against demo.saiku.bi
-Dmondrian.backend=legacyworkaround). All six FoodMart Sales dimensions visible (customer, performance season day, product, promotion, store, time).POST /ai/querySales × Store/Stores/Store Country →status: SUCCESS/ USA / 266,773 Unit Sales.tools/listreturns six tools,list_cubesreturns the six cubes,describe_cube unknown_foodmart/FoodMart/FoodMart/Salessucceeds.systemctl restart saiku-mcp-proxymid-session: shim logs[saiku-shim] session lost — re-handshaking and replaying tools/callto stderr and the client sees its expected response with the right request id.Tests
mvn -pl saiku-core/saiku-service test— 405 tests, 0 failures, 1 skipped (covers the newbuildSchema_broken{Dimension,Hierarchy,Level}DoesNotAbortRemaining…cases).mvn -pl saiku-core/saiku-web test -Dtest=InfoResourceTest— 12 tests including the newdxtShim_recoversFromMidSessionServerCycle.mvn -pl saiku-mcp test— 17 tests including the newjsonResultPassesObjectBodiesThroughUnwrapped.Test plan
Related
npx mcp-remotepath and a pre-collapse UI shape)RelBuilder.field()reports empty input fields against H2 FoodMart, only Time slips through to AI schema mondrian-saiku#30 — root-cause filed, fixed in 4.8.1.9, verified here