Skip to content

chore/fix: dependabot bumps + AI/MCP demo hardening + mondrian 4.8.1.9#875

Merged
buggtb merged 8 commits into
developmentfrom
chore/dependabot-bumps
May 16, 2026
Merged

chore/fix: dependabot bumps + AI/MCP demo hardening + mondrian 4.8.1.9#875
buggtb merged 8 commits into
developmentfrom
chore/dependabot-bumps

Conversation

@buggtb
Copy link
Copy Markdown
Contributor

@buggtb buggtb commented May 16, 2026

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

# Commit Scope
1 06b1bc2b deps: spring 6.2.8 → 6.2.11, spring-security 6.5.1 → 6.5.10, postgresql 42.7.4 → 42.7.11 (closes #841, #842, #843)
2 a1b9dbfb ui: collapse API access cards by default on login; restore the DXT download button that the CSS had been waiting on
3 18443e90 mcp: DXT manifest switched from server.type: "remote" (Claude Desktop validator rejected with "Expected python|node|binary") to the node + shim form
4 60647f67 mcp: DXT shim is now a self-contained stdio↔streamable-http bridge using only node stdlib — replaces the npx mcp-remote spawn that failed because npx isn't on Claude's built-in-node PATH
5 b50a14e3 mcp: jsonResult wraps array tool bodies under { items: [...] } so strict MCP hosts (mcp-proxy pydantic, Claude) don't reject list_cubes/search_members with dict_type validation errors
6 a2380cad ai-schema: per-loop try/catch in OlapAiCubeMetadataService.buildSchema so one broken level/hierarchy/dimension no longer truncates the rest (closes #877)
7 a4aea53c mcp: DXT shim auto-recovers from mid-session server cycles — detects session loss (HTTP 404, /session/i match), re-handshakes and replays the in-flight request transparently
8 784ce1b7 mondrian: bump to 4.8.1.9 (resolves spiculedata/mondrian-saiku#30 — Calcite-H2 introspection); slf4j import swap in mondrian.rolap.SqlStatement override for 4.8.1.9's log4j-1.x drop

Verified end-to-end against demo.saiku.bi

  • Calcite is the default backend again (no -Dmondrian.backend=legacy workaround). All six FoodMart Sales dimensions visible (customer, performance season day, product, promotion, store, time).
  • POST /ai/query Sales × Store/Stores/Store Country → status: SUCCESS / USA / 266,773 Unit Sales.
  • DXT installs cleanly in Claude Desktop, tools/list returns six tools, list_cubes returns the six cubes, describe_cube unknown_foodmart/FoodMart/FoodMart/Sales succeeds.
  • Operator-side systemctl restart saiku-mcp-proxy mid-session: shim logs [saiku-shim] session lost — re-handshaking and replaying tools/call to 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 new buildSchema_broken{Dimension,Hierarchy,Level}DoesNotAbortRemaining… cases).
  • mvn -pl saiku-core/saiku-web test -Dtest=InfoResourceTest — 12 tests including the new dxtShim_recoversFromMidSessionServerCycle.
  • mvn -pl saiku-mcp test — 17 tests including the new jsonResultPassesObjectBodiesThroughUnwrapped.

Test plan

  • Tests above all green locally
  • Demo on Calcite serves all six FoodMart dimensions and executes Store-country query
  • DXT loads in Claude Desktop and surfaces the 6 Saiku tools after a fresh install
  • Mid-session mcp-proxy restart no longer requires user intervention to recover
  • CI green on this PR

Related

…, 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.
buggtb added 7 commits May 16, 2026 17:37
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.
@buggtb buggtb changed the title chore(deps): consolidate spring/spring-security/postgresql bumps (closes #841, #842, #843) chore/fix: dependabot bumps + AI/MCP demo hardening + mondrian 4.8.1.9 May 16, 2026
@buggtb buggtb merged commit 7964644 into development May 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant