Combining present hype (MCP)
with past (GraphQL),
the road to malleable* software
Arthur Juchereau
Principal Engineer at Welbi
Hey, my name is
GraphQL
We're June 19th 2015
you are cool if you use GraphQL
First there was the schema*
type Attendance {
program: Program!
resident: Resident!
status: String!
}
type Program {
attendees: [Resident!]!
id: ID!
title: String!
}
type Resident {
attendance: [Attendance!]!
id: ID!
name: String!
}
type Mutation {
createProgram(title: String!): Program!
createResident(name: String!): Resident!
deleteAttendance(programId: String!, residentId: String!): Boolean!
deleteProgram(id: String!): Boolean!
deleteResident(id: String!): Boolean!
recordAttendance(programId: String!, residentId: String!, status: String!): Attendance!
updateAttendance(programId: String!, residentId: String!, status: String!): Attendance!
updateProgram(id: String!, title: String!): Program!
updateResident(id: String!, name: String!): Resident!
}
type Query {
attendanceByProgram(programId: String!): [Attendance!]!
attendanceByResident(residentId: String!): [Attendance!]!
program(id: String!): Program
programs: [Program!]!
resident(id: String!): Resident
residents: [Resident!]!
}
Then the request
query {
resident(id:"1"){
id
name
attendance{
program{
name
}
status
}
}
second:resident(id:"2"){
id
name
attendance{
status
}
}
}
Validated
against the schema
Resolve
query {
resident(id:"1"){
id
name
attendance{
program{
name
}
status
}
}
second:resident(id:"2"){
id
name
}
builder.queryType({
fields: (t) => ({
resident: t.field({
type: ResidentRef,
nullable: true,
args: {
id: t.arg.string({ required: true }),
},
resolve: (_, { id }) => findResidentById(id),
}),
}),
});
[
{
"id": "1",
"name": "John Smith"
},
{
"id": "2",
"name": "Mary Johnson"
}
]
const ResidentRef = builder.objectRef<Resident>('Resident').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
attendance: t.field({
type: [AttendanceRef],
resolve: (resident) => {
const attendance = readAttendance();
return attendance.filter(a => a.residentId === resident.id);
},
}),
}),
});
Side note on Auth
resident: t.field({
type: ResidentRef,
nullable: true,
args: {
id: t.arg.string({ required: true }),
},
resolve: (_, { id }, context) => {
// Auth check: verify user has permission to read this specific resident
const requiredGrant = `read_resident_${id}`;
if (!context.grants?.has(requiredGrant)) {
throw new Error(`Unauthorized: Missing grant '${requiredGrant}'`);
}
return findResidentById(id);
},
}),
Validation of the output
{
"data": {
"resident": {
"id": "1",
"name": "John Smith",
"attendance": [
{
"program": {
"title": "Morning Exercise"
},
"status": "present"
},
{
"program": {
"title": "Art Therapy"
},
"status": "absent"
}
]
},
"second": {
"id": "2",
"name": "Mary Johnson"
}
}
}
Validated
against the schema
Send to client
GraphQL Pros
- Works with any client that can POST
- Fetch what you need
- Schema contract (multi Schema!!)
- LLMs know how to query
- Observability is great
- Amazing versioning patterns
- Resource based Authorization checks
GraphQL Cons
- Server-side waterfall
- Difficult to implement at first
- Runtime Cost of validation
- Tree response can grow quite fast
- Somewhat rigid schema definition
Model Context Protocol
We're June 19th 2025
you are cool if you use MCP
First there was the schema
const databaseRead = tool({
name: "database_query_readonly",
description:
"Readonly database query for MySQL, use this if there are no direct tools",
args: z.object({ query: z.string() }),
async run(input) {
return db.transaction(async (tx) => tx.execute(input.query), {
accessMode: "read only",
isolationLevel: "read committed",
})
},
})
const databaseWrite = tool({
name: "database_query_write",
description:
"DANGEROUS operation that writes to the database. You MUST triple check with the user before using this tool - show them the query you are about to run.",
args: z.object({ query: z.string() }),
async run(input) {
return db.transaction(async (tx) => tx.execute(input.query), {
isolationLevel: "read committed",
})
},
})
Then you register the servers

Embrace the tool loop

Embrace the tool loop

MCP pros
- MCP servers can do anything you want (play piano, control your lights, save to DBs, crawl the web, call other LLMs)
- Quite good* at just making it work
- Quite good* at interpreting results and moving to next steps
- somewhat universal** text API
MCP cons
- LLMs are just slow
- Security story is not great
- Auth story is not great
- Observability is not great
- Guard rails are difficult to enforce
- LLMs are very un-evenly trained on tool calling

Why not both?
Why not both?
const getGraphQLSchema = tool({
name: "get_graphql_schema",
description: "Get the generated GraphQL schema for the application",
args: z.object({}),
async run() {
const schemaPath = join(process.cwd(), "generated-schema.graphql")
const schema = readFileSync(schemaPath, "utf-8")
return {
schema,
message: "GraphQL schema loaded successfully"
}
},
})
const graphqlQuery = tool({
name: "graphql_query",
description: "Execute a GraphQL query against the API. Use this to fetch data from the GraphQL endpoint.",
args: z.object({
query: z.string().describe("The GraphQL query to execute"),
variables: z.record(z.any()).optional().describe("Optional variables for the GraphQL query")
}),
async run(input) {
const { query, variables } = input
const response = await fetch(process.env.GRAPHQL_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
},
body: JSON.stringify({ query, variables }),
})
return {
success: true,
response,
note: "This is a demo response. In production, this would make an actual HTTP request to your GraphQL endpoint."
}
},
})

Pros
- Users can now define their own workflows
- All the auth/audit/logic is centralized
- New features magically appear (csv upload for instance)
- Glue to other MCP server ecosystem
Cons
- Typing in a chat or via voice is not a good user experience
- Everything is slow (runtime)
- The LLMs can get lost easily and start looping
- Registering a new server / model may destroy a flow that was working fineĀ
Malleable Software
Malleability refers to the ability of a material to be deformed under pressure without breaking
Malleable Software
The problem is clients are applying pressure in different, often conflicting, directions
WordPress
WordPress (self Hosted)
White labeling
Technical skills
Malleability
linear
website builders
Spreadsheets
maintenance burden
Malleability
Feature flags all the things
We know better than you
From the user perspective
The current landscape to choose a tool is like a food court
Lots of options, yet none that is just right
From the user perspective
What if it was more like a kitchen?
With tools and ingredients ready to be used
From the builder perspective
We've been trained to say no to change requests.
A positive change for someone is a breaking one to another
From the builder perspective
What if we could actually author our perfect vision, the authoritative best flow.
While not alienating users that are perfectly adhering to it.
Following the perfect vision,
highest velocity
Detours to get the users
and the money
to keep going
Following the perfect vision,
highest velocity
Can we have a CSV import tool?
split this view in two
Following the perfect vision,
highest velocity
Can we have a CSV export tool?
split this view in two
Marketing teams
Operations
In practice
Product scope
Do it all
Product scope
My vision
Product scope
Micro-frontends
Ops
Marketing
Sales
Resident
Families
Admin
Product scope
pico-frontends?
Ops
Marketing
Sales
Resident
Families
Admin
Product scope
nano-frontends?
Ops
Marketing
Sales
Resident
Families
Admin
Requirements
- Strong Authentification layer (IDP)
- Strong data layer with authorization
- Strong design system for code generation
- Strong Observability & guard rails
- Strong static type-checking
- Strong risk-tolerance
Thank you
deck
By pookmook
deck
- 26