feat(mcp): GraphQL Code Generator + camelCase tool inputs#176
Draft
dsklyar wants to merge 13 commits into
Draft
Conversation
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.
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.
@transcend-io/airgap.js-types
@transcend-io/cli
@transcend-io/internationalization
@transcend-io/privacy-types
@transcend-io/sdk
@transcend-io/type-utils
@transcend-io/utils
@transcend-io/mcp
@transcend-io/mcp-server-admin
@transcend-io/mcp-server-assessment
@transcend-io/mcp-server-base
@transcend-io/mcp-server-consent
@transcend-io/mcp-server-discovery
@transcend-io/mcp-server-dsr
@transcend-io/mcp-server-inventory
@transcend-io/mcp-server-preferences
@transcend-io/mcp-server-workflows
commit: |
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.
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.
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
packages/mcp/mcp-server-{admin,assessment,discovery,dsr,inventory,workflows}/src/graphql.tsnow uses the generatedgraphql()tag and produces aTypedDocumentNode<Result, Variables>.TranscendGraphQLBase.makeRequestacceptsstring | TypedDocumentNodeand infers result/variables types automatically.tsctime rather than as a runtime error. Fixed three pre-existing drifts during the migration (createApiKeypayload,updateWorkflowConfigshape,DataSilo.updatedAtremoval).withDescriptionshelper enforces author-written copy on top of generated Zod input schemas.CursorPaginationSchema/OffsetPaginationSchematomcp-server-basefor shared pagination shapes.CamelCase tool inputs (breaking)
Repo-wide guardrails
Schema lifecycle
Docs
Test plan
Notes / deferred
Made with Cursor