Skip to content

feat(mcp): GraphQL Code Generator + camelCase tool inputs#176

Draft
dsklyar wants to merge 13 commits into
mainfrom
zel-7752/graphql-codegen-ci
Draft

feat(mcp): GraphQL Code Generator + camelCase tool inputs#176
dsklyar wants to merge 13 commits into
mainfrom
zel-7752/graphql-codegen-ci

Conversation

@dsklyar

@dsklyar dsklyar commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Linear: ZEL-7752

Adopts GraphQL Code Generator (client-preset + typescript-validation-schema) across all 6 GraphQL-backed MCP server packages, migrates every tool input parameter to camelCase, adds repo-wide guardrails for AI-friendly Zod descriptions, and wires up a scheduled schema refresh.

What changed

Type-safe GraphQL operations

  • Every operation in packages/mcp/mcp-server-{admin,assessment,discovery,dsr,inventory,workflows}/src/graphql.ts now uses the generated graphql() tag and produces a TypedDocumentNode<Result, Variables>.
  • TranscendGraphQLBase.makeRequest accepts string | TypedDocumentNode and infers result/variables types automatically.
  • Schema drift now fails at tsc time rather than as a runtime error. Fixed three pre-existing drifts during the migration (createApiKey payload, updateWorkflowConfig shape, DataSilo.updatedAt removal).
  • New withDescriptions helper enforces author-written copy on top of generated Zod input schemas.
  • Added CursorPaginationSchema / OffsetPaginationSchema to mcp-server-base for shared pagination shapes.

CamelCase tool inputs (breaking)

  • All snake_case Zod fields across the MCP packages renamed to camelCase. See `8fff032` for the full list.
  • Reflected in changesets as a minor bump for every `@transcend-io/mcp-server-*` package.

Repo-wide guardrails

  • `scripts/check-mcp-descriptions.test.ts` instantiates every MCP tool, unwraps its Zod schema, and asserts every top-level input field has a non-empty `.describe()` (>= 8 chars). Caught and fixed missing descriptions on consent `limit`/`offset` fields.
  • Unit tests for the new `withDescriptions` helper.

Schema lifecycle

  • `schema.graphql` committed at the repo root with descriptions stripped (zero `"""` blocks, ~491 KB).
  • `pnpm graphql:refresh-schema` anonymously introspects `api.staging.transcen.dental`, strips descriptions, and writes the SDL.
  • `.github/workflows/refresh-graphql-schema.yml` runs every Tuesday and opens a chore PR if staging has drifted.
  • Build/typecheck/test/codegen are fully hermetic — no network or secrets needed for normal CI.

Docs

  • `CONTRIBUTING.md` documents the new tool-input conventions (camelCase, descriptions, pagination, `withDescriptions`) and the GraphQL operation/codegen workflow.

Test plan

  • `pnpm lint` passes locally
  • `pnpm format:check` passes locally
  • `pnpm typecheck` passes (19/19 turbo tasks)
  • `pnpm build` passes (all packages)
  • `pnpm test` passes (all suites including the new audit + helper tests)
  • `pnpm check:exports`, `pnpm check:publint`, `pnpm check:packages`, `pnpm check:deps` all pass
  • `pnpm check:changeset` passes (3 changesets covering base/servers/cli+sdk catalog bump)
  • `pnpm graphql:refresh-schema` produces a byte-stable schema with zero descriptions
  • Stdio MCP smoke harness — all 8 servers initialize and list 70 tools total
  • Watch CI on this PR
  • Watch `Preview Release` produce `pkg.pr.new` packages

Notes / deferred

  • Storage of `schema.graphql` in the public repo was discussed and accepted. Backend team's pattern uses Apollo Studio as canonical; ours uses staging introspection because the bytes are identical when in-sync and zero-friction wins for dev velocity. Descriptions are stripped in case of any internal prose. Documented in `CONTRIBUTING.md`.
  • Live read/write end-to-end against staging deferred — request shape is validated by codegen + 401 round-trip; response shape is covered by mocked unit tests.

Made with Cursor

dsklyar added 11 commits June 2, 2026 10:19
Lays the groundwork for catching schema drift at compile time across all six
GraphQL-backed MCP server packages. No operations are migrated to the typed
graphql() tag yet -- this commit only wires the tooling so subsequent commits
can migrate one server at a time.

Foundation pieces:

- pnpm-workspace.yaml + root package.json: catalog graphql, @graphql-codegen/cli,
  @graphql-codegen/client-preset, @graphql-codegen/typescript,
  graphql-codegen-typescript-validation-schema, and @graphql-typed-document-node/core.
  Add 'pnpm codegen' and 'pnpm graphql:refresh-schema' scripts.
- codegen.ts at repo root drives client-preset (typed graphql() tag +
  TypedDocumentNode) and typescript-validation-schema (Zod schemas mirroring
  GraphQL input types) for each MCP server's src/__generated__/.
- schema.graphql committed at repo root as the offline source of truth so PR
  CI stays hermetic. scripts/refresh-graphql-schema.ts performs unauth
  introspection against api.staging.transcen.dental and prints stable SDL.
- turbo.json: codegen task feeds build/typecheck/test via globalDependencies on
  schema.graphql + codegen.ts. .gitignore + lint/format ignore __generated__/.
- mcp-server-base: TranscendGraphQLBase.makeRequest now accepts
  TypedDocumentNode<TResult, TVariables> alongside string queries, calling
  graphql.print() to serialize. Result/variable types are inferred from the
  document, eliminating the response-shape lies that masked the createApiKey
  regression.
- mcp-server-base validation: add withDescriptions helper, plus
  CursorPaginationSchema and OffsetPaginationSchema for shared pagination
  inputs. Existing PaginationSchema is preserved for backwards compatibility.
- All six GraphQL-backed mcp-server-* packages declare graphql +
  @graphql-typed-document-node/core as runtime deps so generated TypedDocumentNode
  imports resolve at consumer install time.
- packages/cli + packages/sdk move their existing graphql dep onto the catalog
  to satisfy syncpack.
Migrate every mcp-server-*/src/graphql.ts to use the generated graphql()
tag from @graphql-codegen/client-preset, replacing string-literal queries
with TypedDocumentNodes. Each operation is now compile-time validated
against the committed schema.graphql, so any drift surfaces in tsc rather
than at runtime.

Schema drift fixed during migration:
- admin.createApiKey: payload returns CreatedApiKey (not ApiKey + token);
  selection set and mapping rewritten to extract apiKey/token correctly.
  ScopePreview.id is now selected directly instead of synthesized from idx.
- workflows.updateWorkflowConfig: UpdateWorkflowConfigPayload only exposes
  success/clientMutationId, so the call is split into a mutation plus a
  follow-up workflowConfig read. showInPrivacyCenter is removed entirely
  (not in schema). Tool input renamed to workflowConfigId (camelCase).
- inventory: removed DataSilo.updatedAt (not in schema); listDataCategories
  tolerates nullable name/id from the DataCategory type.
- discovery: classification scan operations are not in the live schema;
  preserved as plain strings with TODO comments to avoid blocking codegen
  while a follow-up addresses the missing API.

Other adjustments:
- assessment: response mappings adjusted for Assessment shape, including
  status casting.
- dsr: mappings updated for listRequests/getRequest/employeeMakeDataSubject
  Request/cancelRequest to match the typed result shapes.

Tests for admin and workflows updated to mirror the new payload shapes
and camelCase inputs. Full suite (test:root + workspace tests) green.
Adds a weekly (Tuesdays 12:00 UTC) job plus manual workflow_dispatch that:

1. Runs `pnpm graphql:refresh-schema` against staging (introspection is
   open, so no API key is required by default).
2. Runs `pnpm codegen` so any schema drift surfaces as TS errors in the
   resulting PR rather than silently in production.
3. Opens (or updates) chore/refresh-graphql-schema with the regenerated
   files. CI on that PR is the gate; if it fails, an MCP server is
   broken against the new staging schema.

Concurrency group ensures we never run two refreshes at once.
Renames every snake_case Zod field on MCP tool input schemas to camelCase
across all MCP server packages. Tool names themselves keep their snake_case
form because they are externally addressable identifiers.

Affected packages:
- mcp-server-assessment: assessment_id, assessment_section_ids,
  assessment_question_id, assessment_answer_ids, assessment_answer_values,
  assessment_group_id, assessment_name, template_id, reviewer_ids,
  due_date, assignee_ids, assignee_emails, external_assignee_emails,
  submit_for_review.
- mcp-server-consent: tracking_purposes, is_junk, data_flows,
  show_zero_activity, order_field, order_direction.
- mcp-server-discovery: data_silo_id, scan_id, entity_types.
- mcp-server-dsr: request_id, data_silo_id, profile_ids.
- mcp-server-inventory: data_silo_id, data_point_id.
- mcp-server-preferences: user_id.

Each rename touches the Zod schema, the destructured handler argument, any
GraphQL request mappings, the prose in tool descriptions/error messages
that referenced the old field name, and the matching tests. Behavior is
otherwise unchanged.
scripts/check-mcp-descriptions.test.ts walks every getXyzTools()
factory across the eight MCP servers, instantiates each tool with a
mock client (safe because schema construction never touches the
client), and asserts that each top-level Zod input field carries a
non-empty .describe() string >= 8 chars.

Descriptions are the primary signal an LLM client has for "what does
this argument mean", so missing/short descriptions silently degrade
the tool. The audit catches that drift in CI rather than during a
live integration.

Initial run flagged four real gaps in the consent server, all fixed
in this commit (consent_list_purposes, consent_list_data_flows,
consent_list_cookies, consent_list_regimes — pagination fields lacked
.describe()).

Test infra:
- Added the eight @transcend-io/mcp-server-* packages and zod as
  workspace devDependencies on the root manifest so the script project
  can import them via the @transcend-io/source condition.
- scripts/tsconfig.json now includes the DOM lib because the audit
  transitively pulls in @transcend-io/airgap.js-types, which references
  global DOM types. Other packages keep their own narrower lib lists.
Adds unit tests for withDescriptions covering the four behaviors callers
rely on: descriptions are attached, the wrapped schema retains its
validation rules, missing descriptions throw, and empty strings throw.
The TypeScript-level guarantee (every field must have a key in the
descriptions map) is exercised via @ts-expect-error in the missing-key
case so a future regression in the type signature also fails the test.
Three changesets cover the publishable surface affected by the migration:
mcp-server-base (minor) for the new makeRequest/TypedDocumentNode + utility
exports, every mcp-server-* + mcp umbrella (minor) for the typed graphql()
operations and breaking camelCase tool input renames, and cli/sdk (patch)
for the graphql -> catalog dependency move.
Document the camelCase tool input rule, the description audit, the
TypedDocumentNode-based GraphQL workflow, and how to refresh
schema.graphql so future contributors don't have to reverse-engineer
the conventions from CI failures.
The introspection query previously requested descriptions, which baked
1,626 author-written description blocks into the public `schema.graphql`
and JSDoc-style comments into every generated `zod-inputs.ts` (shipped
in published .d.ts bundles).

Switch to `getIntrospectionQuery({ descriptions: false })` so we keep
the type/field shapes (which are necessary for codegen and already
public via the SDK/CLI/MCP packages) but stop surfacing internal prose
that may include deprecation notes, partner names, or business-rule
context.

Engineers who need the descriptions can still introspect staging
directly. Schema file shrinks 1.25 MB -> 491 KB.
Switch the schema-refresh source from anonymous staging introspection to
Apollo Studio's `Transcend-io@current` graph variant, which the backend
release pipeline treats as the canonical published artifact.

Why:
- Aligns with `transcend-io/main`'s release workflow, which runs
  `rover graph publish` to put the schema into Apollo Studio.
- The Apollo Studio source is auth-gated behind `APOLLO_STUDIO_KEY`,
  so the source itself is no longer anonymously fetchable the way
  `api.staging.transcen.dental` happens to be.
- Removes our dependency on staging introspection being open, which is
  an operational quirk rather than a deliberate public contract.

Behavior:
- `pnpm graphql:refresh-schema` now spawns `rover graph fetch
  Transcend-io@current`, strips author-written descriptions, and writes
  `schema.graphql`.
- When `APOLLO_STUDIO_KEY`/`APOLLO_KEY` is missing the script exits 0
  with a warning and leaves the committed schema untouched, so local
  development and CI continue to work for contributors without a key.
- Removes the scheduled `.github/workflows/refresh-graphql-schema.yml`
  cron. Schema refreshes are now manual; an engineer with an Apollo
  Studio key runs the script when the backend ships a change we need.

Documentation in CONTRIBUTING.md is updated with the new workflow,
including how to install Rover and which env var to set.
Revert the Apollo Studio + Rover source switch from e7beca5. With the
storage decision settled (commit `schema.graphql` to the public repo,
descriptions stripped), the auth-gating benefit of Apollo Studio
disappears -- the bytes are public regardless of where they came from.

What we get back from staging introspection:
- Zero-friction refresh: anyone can run `pnpm graphql:refresh-schema`,
  no Apollo Studio key, no Rover install.
- Scheduled cron is feasible again (no secret needed in CI).
- External contributors and bots can refresh without coordination.
- Apollo Studio's `Transcend-io@current` is itself produced by
  `rover graph introspect <staging>`, so the bytes are identical when
  both are in sync.

Trade-off accepted: staging may include types that haven't shipped to
prod yet. Documented in CONTRIBUTING.md as a heads-up before publishing.

Re-adds `.github/workflows/refresh-graphql-schema.yml` to run weekly and
open a PR with the diff. Keeps the description-stripping behavior so
internal prose still cannot leak via the committed file.
@linear-code

linear-code Bot commented Jun 2, 2026

Copy link
Copy Markdown

ZEL-7752

PR #173 (`fix(mcp-server-admin): correct CreateApiKey GraphQL mutation`)
landed on main while this branch was open. It addresses the same
CreateApiKey schema drift our 47801a1 already fixes, but as a
hand-rolled mutation rather than via the typed `graphql()` migration.

Resolution:
- packages/mcp/mcp-server-admin/src/graphql.ts: keep the typed
  graphql() form (strict superset of #173's fix).
- packages/mcp/mcp-server-admin/tests/admin.test.ts: keep all of
  #173's regression tests, with the two assertions that hard-coded
  the pre-migration mutation name (`CreateApiKey`) and return shape
  (`{ apiKey, token }`) updated to match the post-migration realities
  (`AdminCreateApiKey`, flat `CreatedApiKey`). The structural guards
  from #173 -- `scopes { id name }` is selected, no bare top-level
  `token`, plain-text token never leaks back onto the result -- are
  preserved verbatim.
- Both #173's changeset (`fix-admin-create-api-key-mutation.md`) and
  ours (`zel-7752-graphql-codegen-*.md`) are kept; changesets dedupe
  on next version PR with the highest bump winning.
@pkg-pr-new

pkg-pr-new Bot commented Jun 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@transcend-io/airgap.js-types

pnpm add https://pkg.pr.new/@transcend-io/airgap.js-types@176
yarn add https://pkg.pr.new/@transcend-io/airgap.js-types@176.tgz

@transcend-io/cli

pnpm add https://pkg.pr.new/@transcend-io/cli@176
yarn add https://pkg.pr.new/@transcend-io/cli@176.tgz

@transcend-io/internationalization

pnpm add https://pkg.pr.new/@transcend-io/internationalization@176
yarn add https://pkg.pr.new/@transcend-io/internationalization@176.tgz

@transcend-io/privacy-types

pnpm add https://pkg.pr.new/@transcend-io/privacy-types@176
yarn add https://pkg.pr.new/@transcend-io/privacy-types@176.tgz

@transcend-io/sdk

pnpm add https://pkg.pr.new/@transcend-io/sdk@176
yarn add https://pkg.pr.new/@transcend-io/sdk@176.tgz

@transcend-io/type-utils

pnpm add https://pkg.pr.new/@transcend-io/type-utils@176
yarn add https://pkg.pr.new/@transcend-io/type-utils@176.tgz

@transcend-io/utils

pnpm add https://pkg.pr.new/@transcend-io/utils@176
yarn add https://pkg.pr.new/@transcend-io/utils@176.tgz

@transcend-io/mcp

pnpm add https://pkg.pr.new/@transcend-io/mcp@176
yarn add https://pkg.pr.new/@transcend-io/mcp@176.tgz

@transcend-io/mcp-server-admin

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-admin@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-admin@176.tgz

@transcend-io/mcp-server-assessment

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-assessment@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-assessment@176.tgz

@transcend-io/mcp-server-base

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-base@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-base@176.tgz

@transcend-io/mcp-server-consent

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-consent@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-consent@176.tgz

@transcend-io/mcp-server-discovery

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-discovery@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-discovery@176.tgz

@transcend-io/mcp-server-dsr

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-dsr@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-dsr@176.tgz

@transcend-io/mcp-server-inventory

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-inventory@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-inventory@176.tgz

@transcend-io/mcp-server-preferences

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-preferences@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-preferences@176.tgz

@transcend-io/mcp-server-workflows

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-workflows@176
yarn add https://pkg.pr.new/@transcend-io/mcp-server-workflows@176.tgz

commit: 2dfeb64

CI's `pnpm typecheck` was failing because `//#typecheck:root` ran in
parallel with `//#codegen` (no declared dependency), so the root tsc
pass against `scripts/tsconfig.json` saw missing
`__generated__/gql.js` modules in the workspace MCP packages.

The local `pnpm typecheck` masked this because we always ran
`pnpm codegen` first by hand; turbo just found the artifacts on disk
and considered the task satisfied.

Add `dependsOn: ["//#codegen"]` to both root tasks so turbo serializes
them after codegen even on a cold cache.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant