Guide · GraphQL API
MCP Tools for GraphQL Nexus — type-safe schema, fieldAuthorizationPlugin, Prisma Nexus
Three Nexus behaviours catch MCP tool developers off guard: fieldAuthorizationPlugin runs field authorization after the resolver executes, not before — unlike Pothos's scope-auth (pre-resolver) or directive-based authorization, Nexus's built-in fieldAuthorizationPlugin checks the authorize function on the resolved value, meaning your resolver has already executed and potentially fetched data before authorization is checked; for pre-resolver authorization you need a custom plugin or Nexus Shield; t.model from nexus-plugin-prisma generates CRUD fields automatically but does not guard individual fields with row-level permissions — exposing t.model.user() on the Query type without a resolver override exposes all user columns including any sensitive fields Prisma returns (like passwordHash), because Nexus passes the Prisma result directly to the field without any field-level filtering; and makeSchema's outputs configuration writes generated TypeScript type files to disk during development, but in production containers those files may not exist if the schema was compiled ahead of time — running Nexus in a Docker container without pre-building the type files causes startup failures when shouldExitAfterGenerateArtifacts is true or type file paths are missing.
TL;DR
For pre-resolver auth use nexus-shield or a custom beforeResolve plugin. Never expose raw t.model fields without an explicit select — override with t.field({ resolve: () => prisma.user.findUnique({ select: { id, name } }) }). Set shouldExitAfterGenerateArtifacts: process.env.NODE_ENV !== 'production'. Health probe: POST /graphql with {"query":"{ __typename }"}.
Field authorization — post-resolver vs pre-resolver
Nexus's fieldAuthorizationPlugin adds an authorize option to field definitions. This function receives the resolved value and context — it runs after the resolver has completed. If authorization fails, Nexus nulls the field and adds an error to the response. This is different from many authorization libraries that intercept execution before the resolver runs.
import { makeSchema, queryType, objectType, fieldAuthorizePlugin } from 'nexus';
// fieldAuthorizePlugin: runs AFTER the resolver returns a value
// Useful for object-level auth (can you see this specific post?)
// Not useful for field-access auth (should this query even execute?)
const schema = makeSchema({
types: [
queryType({
definition(t) {
t.field('post', {
type: 'Post',
args: { id: nonNull(idArg()) },
// This authorize function runs AFTER the resolver fetches the post
// The post has already been loaded from the database when this runs
authorize: (_root, _args, ctx) => !!ctx.viewer,
resolve: (_root, args, ctx) =>
ctx.prisma.post.findUnique({ where: { id: args.id } }),
});
},
}),
],
plugins: [fieldAuthorizePlugin()],
});
// For PRE-resolver authorization, use a custom plugin or nexus-shield:
// npm install nexus-shield
import { nexusShield, allow, deny, rule } from 'nexus-shield';
const isAuthenticated = rule({ cache: 'contextual' })(
async (_root, _args, ctx) => !!ctx.viewer
);
const isPostAuthor = rule({ cache: 'strict' })(
async (_root, args, ctx) => {
if (!ctx.viewer) return false;
const post = await ctx.prisma.post.findUnique({
where: { id: args.id },
select: { authorId: true },
});
return post?.authorId === ctx.viewer.id;
}
);
const schemaWithShield = makeSchema({
types: [/* ... */],
plugins: [
nexusShield({
rules: {
Query: {
post: isAuthenticated,
editPost: and(isAuthenticated, isPostAuthor),
publicFeed: allow,
adminPanel: deny, // deny all access — must be overridden per-role
},
Mutation: {
createPost: isAuthenticated,
deletePost: and(isAuthenticated, isPostAuthor),
},
},
options: {
// fallbackRule applies to any field not explicitly listed above
fallbackRule: allow,
// Set to deny to whitelist-only authorization (safer for sensitive schemas)
// fallbackRule: deny,
},
}),
],
});
Nexus Shield rules are evaluated before the resolver executes. The cache option controls whether rule results are memoized within a request: 'contextual' caches per-context (same result for same viewer throughout request), 'strict' caches per parent+args combination, and 'no_cache' (default) re-evaluates for every field access. For expensive rules (database lookups), use 'contextual' or 'strict'.
nexus-plugin-prisma and t.model field exposure
The nexus-plugin-prisma package (also known as @nexus/schema with Prisma) provides t.model — it reads your Prisma schema and generates GraphQL field definitions automatically. Each Prisma model field becomes a GraphQL field. The generated fields pass through the full Prisma record without filtering, which means any field on the Prisma model becomes visible in the API.
// Assuming Prisma schema:
// model User {
// id String @id
// name String
// email String
// passwordHash String // NEVER expose this
// stripeCustomerId String // sensitive
// createdAt DateTime
// }
import { objectType } from 'nexus';
// WRONG: t.model exposes ALL Prisma fields unless you explicitly exclude
const UserTypeBroken = objectType({
name: 'User',
definition(t) {
// This exposes id, name, email, passwordHash, stripeCustomerId, createdAt
t.model.id();
t.model.name();
t.model.email();
// t.model.passwordHash() // at least you didn't add this line...
// But if you use t.model.all() you'd expose everything including passwordHash
},
});
// RIGHT: only expose fields you explicitly want; override resolver for sensitive queries
const UserType = objectType({
name: 'User',
definition(t) {
// Explicitly list only the fields safe to expose
t.model.id();
t.model.name();
// email: only expose to the viewer themselves
t.field('email', {
type: 'String',
nullable: true,
resolve: (user, _args, ctx) =>
// Only return email to the user themselves or admins
ctx.viewer?.id === user.id || ctx.viewer?.role === 'admin'
? user.email
: null,
});
// t.model.passwordHash() — omitted entirely, not exposed
// t.model.stripeCustomerId() — omitted entirely
},
});
// IMPORTANT: Nexus will generate TypeScript types based on what you expose
// Omitting a field from the objectType definition removes it from the GraphQL schema
// but does NOT prevent Prisma from fetching it when resolving the parent query
// To avoid fetching sensitive columns: use Prisma's select in the resolver:
const QueryType = queryType({
definition(t) {
t.field('user', {
type: 'User',
args: { id: nonNull(idArg()) },
authorize: (_root, _args, ctx) => !!ctx.viewer,
resolve: (_root, args, ctx) =>
ctx.prisma.user.findUnique({
where: { id: args.id },
// Explicit select: passwordHash never fetched, never in memory
select: { id: true, name: true, email: true, createdAt: true },
}),
});
},
});
makeSchema outputs and production container startup
Nexus generates two artifact files: nexus-typegen.ts (TypeScript types for context-aware resolver arguments) and schema.graphql (SDL representation of the schema). During development, Nexus writes these files automatically when you run the server. In production Docker containers, these files must be pre-built before the container image is finalized — otherwise Nexus either regenerates them at startup (slow, requires write access) or fails if shouldExitAfterGenerateArtifacts is set.
import { makeSchema } from 'nexus';
import path from 'path';
export const schema = makeSchema({
types: [/* all your type definitions */],
outputs: {
// Generated TypeScript types — imported by your resolvers for type safety
typegen: path.join(__dirname, '../nexus-typegen.ts'),
// SDL schema — useful for documentation and schema diffing in CI
schema: path.join(__dirname, '../schema.graphql'),
},
// In development: generate artifacts and continue serving
// In production: skip artifact generation (files should already exist from build step)
shouldExitAfterGenerateArtifacts: process.env.NODE_ENV === 'development'
? !process.env.SKIP_TYPEGEN
: false,
contextType: {
// Point to your Context type for type-safe resolvers
module: path.join(__dirname, './context.ts'),
export: 'Context',
},
});
// Docker multi-stage build strategy:
// Stage 1: builder — runs 'ts-node --transpile-only src/generate-schema.ts'
// This script imports schema, which triggers artifact generation
// Then copies nexus-typegen.ts and schema.graphql into the build context
// Stage 2: runner — copies compiled JS + pre-built artifacts
// NODE_ENV=production prevents Nexus from attempting re-generation
// generate-schema.ts (runs in build stage only)
import { schema } from './schema';
import fs from 'fs';
// Force artifact generation (in build stage)
process.env.NODE_ENV = 'development';
// schema import triggers makeSchema which writes nexus-typegen.ts and schema.graphql
// Exit after generation (shouldExitAfterGenerateArtifacts catches this)
// Verify artifacts exist before starting the server:
function assertArtifactsExist() {
const typegenPath = path.join(__dirname, '../nexus-typegen.ts');
const schemaPath = path.join(__dirname, '../schema.graphql');
if (!fs.existsSync(typegenPath)) {
throw new Error(
\`Missing nexus-typegen.ts at \${typegenPath}. ` +
\`Run 'npm run generate' before starting in production.\`
);
}
if (!fs.existsSync(schemaPath)) {
throw new Error(\`Missing schema.graphql at \${schemaPath}\`);
}
}
if (process.env.NODE_ENV === 'production') {
assertArtifactsExist();
}
A common CI/CD pattern: add a generate script to package.json that runs ts-node src/schema.ts (which triggers artifact generation via the Nexus side-effect), commit the generated files to the repository, and verify in CI that the committed schema matches the generated one. Schema drift between committed SDL and runtime schema is caught at the PR level.
Health probe — Nexus GraphQL reachability from an MCP server
Nexus schemas are served through standard GraphQL servers — Apollo Server, Yoga, or any framework that accepts a GraphQLSchema object from makeSchema(). The health probe is the same regardless of the Nexus configuration: send { __typename } and verify the response.
async function checkNexusHealth(graphqlUrl: string): Promise<{
healthy: boolean;
latencyMs?: number;
schemaVersion?: string;
error?: string;
}> {
const start = Date.now();
try {
const response = await fetch(graphqlUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '{ __typename }' }),
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) {
return { healthy: false, error: \`HTTP \${response.status}\` };
}
const body = await response.json();
if (body.errors) {
return { healthy: false, error: body.errors[0]?.message };
}
// Optionally: introspect the schema version via a custom field
// builder.queryField('schemaVersion', t => t.nonNull.string({ resolve: () => BUILD_VERSION }))
const schemaVersion = body.data?.schemaVersion;
return {
healthy: true,
latencyMs: Date.now() - start,
schemaVersion,
};
} catch (err) {
return { healthy: false, error: String(err) };
}
}
// Detect schema drift: compare runtime SDL vs committed schema.graphql
async function detectSchemaDrift(graphqlUrl: string, committedSchemaPath: string): Promise<boolean> {
const fs = await import('fs/promises');
const { buildClientSchema, printSchema, getIntrospectionQuery } = await import('graphql');
const committed = await fs.readFile(committedSchemaPath, 'utf8');
const response = await fetch(graphqlUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: getIntrospectionQuery() }),
});
const { data } = await response.json();
const runtime = printSchema(buildClientSchema(data));
const drifted = committed.trim() !== runtime.trim();
if (drifted) {
console.warn('Schema drift detected — runtime schema differs from committed schema.graphql');
}
return drifted;
}
AliveMCP monitors Nexus GraphQL endpoints with a schema-drift probe in addition to the standard { __typename } check — comparing the runtime introspection result against the committed schema.graphql file and alerting when they diverge, catching accidental schema changes pushed to production without corresponding client updates.
Related guides
- MCP Tools for Apollo Server — DataLoader batching, query complexity, persisted queries
- MCP Tools for Pothos — scope auth, complexity plugin, Prisma integration
- MCP Tools for GraphQL Yoga — plugin ordering, SSE subscriptions, CORS config
- MCP Tools for Strawberry — async context, per-request DataLoader, permission classes
- GraphQL schema design for MCP tools
- MCP server authentication overview