Skip to content

fix(mcp): advertise quantile level as a string enum - #2971

Merged
kodiakhq[bot] merged 2 commits into
hyperdxio:mainfrom
RIP21:claude/mcp-non-string-enum
Aug 21, 2026
Merged

fix(mcp): advertise quantile level as a string enum#2971
kodiakhq[bot] merged 2 commits into
hyperdxio:mainfrom
RIP21:claude/mcp-non-string-enum

Conversation

@RIP21

@RIP21 RIP21 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes the Gemini half of #2967. The tuple-items half is already handled by #2925 and ships in the next release, so this PR is only the non-string enum.

Why

enum on a non-string type is valid JSON Schema, and every provider in the matrix on #2967 accepts it except Gemini, whose function declarations only allow enum alongside type: "string". A client that forwards MCP tool schemas straight to the provider therefore gets its entire tool list rejected because of one field, and the user sees:

Error: NonRetriableError: Provider Error We're having trouble connecting to the model provider.
This might be temporary - please try again in a moment.

That names neither the tool nor the property, and retrying never helps. Detaching the MCP server is the only workaround today. Same shape of failure as #2925, different construct and a different provider.

level is the only offender:

z.union([z.literal(0.5), z.literal(0.9), z.literal(0.95), z.literal(0.99)])
// -> { "type": "number", "enum": [0.5, 0.9, 0.95, 0.99] }

It reaches four tools through mcpSelectItemSchema and mcpTileSelectItemSchema: clickstack_timeseries, clickstack_table, clickstack_save_dashboard, clickstack_patch_dashboard.

What changed

mcpQuantileLevelSchema in mcp/tools/query/schemas.ts advertises the string enum, and both MCP select-item schemas use it.

Only the advertised wire type changes. The schema still accepts the numeric form, so a caller working from a cached schema keeps working; it coerces back to a number before any consumer sees it, so level stays a number downstream in both the quantile(<level>)(...) SQL synthesis in table.ts and the stored dashboard config; and out-of-set values are still rejected from either input form. If you would rather advertise strings only and reject numbers outright, say so and I will drop the preprocess — I included it because you mentioned server-side coercion and it makes the change invisible to existing callers.

externalQuantileLevelSchema in utils/zod.ts is deliberately untouched. It is the external REST API's public contract, and all the existing numeric level: fixtures under routers/external-api/ still exercise it unchanged.

Tests

Two additions, per suggestion #2 on the issue.

mcp/__tests__/toolSchemas.test.ts gets a sibling to the draft-2020-12 check: no advertised tool schema may carry a non-string enum or an array-form items. Reverting just the schema change makes it fail and name every offender by JSON pointer:

clickstack_timeseries/properties/select/items/properties/level: enum on a non-string type ([0.5,0.9,0.95,0.99])
clickstack_table/properties/select/items/properties/level: ...
clickstack_save_dashboard/properties/tiles/items/anyOf/0/properties/config/properties/select/items/properties/level: ...
clickstack_patch_dashboard/properties/tile/anyOf/0/properties/config/properties/select/items/properties/level: ...

The array-form items assertion is redundant with the metaschema check on paper, since 2020-12 moved tuples to prefixItems. It is there because the metaschema failure surfaces as an Ajv compile error while this one points at the property, which is the difference between a five-minute fix and an afternoon. Worth noting the reverse too: the 2020-12 metaschema accepts a non-string enum happily, which is exactly why this survived #2925.

mcp/__tests__/query.test.ts gets direct coverage of mcpQuantileLevelSchema: both input forms parse to the number, out-of-set and wrong-typed values are rejected, and .optional() does not swallow a bad value.

Verified on this branch:

  • yarn ci:unit in packages/api — 45 suites, 747 tests, all passing before these additions; 749 with them
  • yarn lint — 302 warnings, exactly at the --max-warnings 302 budget, so no new lint debt
  • npx tsc --noEmit — clean

Note on process

I am not on the vouch list, so I expect the needs-vouch label. I opened #2967 and @brandon-pereira invited this PR there, so hopefully that covers the introduction, but happy to open a hello issue if you would rather do it by the book.

Branch is prefixed claude/ per the agent-generated convention in AGENTS.md. Written with Claude, reviewed and verified against a live Cursor session and the local suites by me. No Co-Authored-By trailer, per the same guide.

`enum` on a non-string type is valid JSON Schema, and every provider except
Gemini accepts it. Gemini's function declarations only allow `enum` alongside
`type: "string"`, so a client that forwards MCP tool schemas straight to the
provider gets its entire tool list rejected because of this one field. The
user sees "We're having trouble connecting to the model provider", which names
neither the tool nor the property.

`level` was the only offender, via
`z.union([z.literal(0.5), z.literal(0.9), z.literal(0.95), z.literal(0.99)])`,
which renders as `{ "type": "number", "enum": [0.5, 0.9, 0.95, 0.99] }`. It
reached four tools: clickstack_timeseries, clickstack_table,
clickstack_save_dashboard and clickstack_patch_dashboard.

Only the advertised wire type changes. Numeric input is still accepted for
callers working from a cached schema, the value is coerced back to a number
before any consumer sees it, and out-of-set values are still rejected either
way, so `level` stays a number downstream in both the `quantile(<level>)(...)`
SQL synthesis and the stored dashboard config. `externalQuantileLevelSchema`
is left alone because it is the external REST API's public contract.

Adds a test asserting no advertised tool schema carries a non-string `enum` or
an array-form `items`, alongside the draft-2020-12 check from hyperdxio#2925. The
metaschema check passes non-string enums happily, which is why this survived
that PR.

Refs hyperdxio#2967
@changeset-bot

changeset-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e861bfd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

@RIP21 is attempting to deploy a commit to the HyperDX Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added external needs-vouch Author needs a maintainer to vouch for them labels Aug 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hi @RIP21, thanks for the pull request!

Before we review code from a first-time contributor we ask that a maintainer vouches for you, and you're not on our list yet. This PR stays open — it just isn't in the review queue until someone vouches.

To get vouched, open an issue saying hello and what you're working on:

https://github.com/hyperdxio/hyperdx/issues/new?template=introduce-yourself.md

A maintainer will usually reply within a day or two, and then this PR gets picked up as normal. More detail in our contributing guide.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes MCP quantile levels to advertise string enum values for Gemini compatibility while coercing accepted string and legacy numeric inputs back to numbers.

  • Introduces a shared MCP quantile-level schema for query and dashboard tools.
  • Adds parsing coverage for valid, invalid, and optional quantile levels.
  • Adds tool-schema compatibility checks for non-string enums and draft-07 tuple forms.
  • Adds a patch changeset for the API package.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/api/src/mcp/tools/query/schemas.ts Adds the shared string-advertised quantile schema while preserving numeric parsed output and the existing allowlist.
packages/api/src/mcp/tools/dashboards/schemas.ts Reuses the MCP-specific quantile schema for dashboard tile select items without changing the existing module dependency edge.
packages/api/src/mcp/tests/query.test.ts Covers string and numeric coercion, invalid values, and optional-field behavior.
packages/api/src/mcp/tests/toolSchemas.test.ts Adds recursive compatibility checks across advertised MCP tool schemas.
.changeset/fix-mcp-non-string-enum.md Documents the Gemini compatibility fix and schedules an API patch release.

Reviews (2): Last reviewed commit: "Merge branch 'main' into claude/mcp-non-..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

This is a tightly scoped, backward-compatible schema change. The new mcpQuantileLevelSchema advertises the quantile level field as a string enum while still accepting numeric input (coerced via z.preprocess) and transforming back to number before any downstream consumer, so the SQL synthesis in table.ts (quantile(${match.level})(...), typed level?: number) and the stored dashboard config are unaffected. Independent verification confirmed all four literals round-trip cleanly (String(0.5)"0.5", String(0.9)"0.9", String(0.95)"0.95", String(0.99)"0.99"), out-of-set values are rejected from both input forms, and .optional() composes without swallowing bad values. The changeset is correctly scoped to @hyperdx/api and no other package changes were missed.

🟡 P2 — recommended

  • packages/api/src/mcp/tools/query/schemas.ts:108 — The set of allowed quantile levels is now expressed three independent times (string enum in mcpQuantileLevelSchema, numeric union in externalQuantileLevelSchema at utils/zod.ts:167, and free text in the field's .describe()), with nothing tying them together, so adding or removing a level touches multiple sites and the MCP and REST surfaces can silently diverge.
    • Fix: Derive both schemas and the description string from a single shared QUANTILE_LEVELS constant.
    • maintainability, project-standards
🔵 P3 nitpicks (1)
  • packages/api/src/mcp/tools/query/schemas.ts:297 — The level field's .describe() still lists "Allowed values: 0.5, 0.9, 0.95, 0.99" as numbers while the advertised machine enum is now strings; both forms parse because numeric input is coerced, so this is cosmetic, but the prose no longer matches the wire type a client reads.
    • Fix: Align the description with the advertised string enum, or generate it from the shared constant.

Reviewers (4 of 9 returned before synthesis): maintainability, project-standards, learnings-researcher, previous-comments. Correctness, testing, api-contract, kieran-typescript, and agent-native were dispatched but had not returned at the synthesis cutoff; the orchestrator performed independent correctness and contract verification (coercion round-trip, downstream number-type preservation, .optional() composition) and found no issues. No actionable human feedback existed in prior comments (changeset-bot, vercel-bot, and the vouch notice only), and the repo has no docs/solutions/ learnings to surface.

Testing gaps: The mcpQuantileLevelSchema unit tests are thorough (both input forms, rejections, .optional()), and toolSchemas.test.ts asserts no advertised tool schema carries a non-string enum or array-form items; however, there is no end-to-end assertion that a numeric level sent through clickstack_save_dashboard/clickstack_patch_dashboard is persisted as a number in the stored dashboard config — the transform is exercised only in isolation.

@brandon-pereira brandon-pereira left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - thank you!

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 21, 2026 10:19pm
hyperdx-storybook Ready Ready Preview Aug 21, 2026 10:19pm

Request Review

This was referenced Aug 21, 2026
@brandon-pereira

Copy link
Copy Markdown
Member

/vouch @RIP21

@github-actions github-actions Bot mentioned this pull request Aug 21, 2026
@brandon-pereira brandon-pereira removed the needs-vouch Author needs a maintainer to vouch for them label Aug 21, 2026
@kodiakhq
kodiakhq Bot merged commit b52a6fa into hyperdxio:main Aug 21, 2026
24 of 26 checks passed
kodiakhq Bot pushed a commit that referenced this pull request Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants