Direct GraphQL
An agent does not need MCP to use GraphQL. It needs the endpoint, a way to learn the schema, and the discipline to fix its own errors. This page shows the loop with plain curl; the agent loop wraps it in code.
The endpoint here is http://localhost:4000/graphql, the open grandstack-server.
1. Learn the shape
Read the descriptions first: every type and field in the server’s schema has one (schema guide in the server repo, and llms.txt in the umbrella). Then ask the server:
curl -s localhost:4000/graphql -H 'content-type: application/json' \
-d '{"query":"{ __schema { queryType { fields { name } } } }"}'
{"data":{"__schema":{"queryType":{"fields":[{"name":"moviesConnection"},{"name":"movies"},{"name":"peopleConnection"},{"name":"people"},{"name":"genresConnection"},{"name":"genres"}]}}}}
The generated schema is large. Ask for one type at a time:
{ __type(name: "MovieWhere") { inputFields { name type { name kind ofType { name } } } } }
2. Query
Filters use operators such as eq, contains, gte, startsWith (contains is case-sensitive). ...Connection fields give totalCount.
curl -s localhost:4000/graphql -H 'content-type: application/json' \
-d '{"query":"{ movies(where:{title:{contains:\"Matrix\"}}){ title released genres{name} } }"}'
{"data":{"movies":[{"title":"The Matrix","released":1999,"genres":[{"name":"Action"},{"name":"Sci-Fi"}]},{"title":"The Matrix Reloaded","released":2003,"genres":[{"name":"Action"},{"name":"Sci-Fi"}]}]}}
With variables, and a count:
curl -s localhost:4000/graphql -H 'content-type: application/json' \
-d '{"query":"query($y:Int){ moviesConnection(where:{released:{gte:$y}}){ totalCount } }","variables":{"y":2010}}'
{"data":{"moviesConnection":{"totalCount":40}}}
Sorting takes a list of single-field objects, for example sort: [{ released: ASC }] (the exact enum is in MovieSort; ask for it with describe_type or __type). “Who has directed the most films”:
{ people(sort: [{ directedCount: DESC }], limit: 3) { name directedCount } }
returns Christopher Nolan 8, Steven Spielberg 7, Quentin Tarantino 6. The counts are stored on each person and refreshed by the server after every mutation.
3. Read the errors
curl -s localhost:4000/graphql -H 'content-type: application/json' \
-d '{"query":"{ movies(where:{titel:\"x\"}){ title } }"}'
{"errors":[{"message":"Field \"titel\" is not defined by type \"MovieWhere\". Did you mean \"title\"?","locations":[{"line":1,"column":17}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
GraphQL returns HTTP 200 with an errors array, so an agent must look at the body, not just the status. The message says what to fix. That is the loop: write, run, read the error, correct.
4. Mutate carefully
mutation($t: String!) {
createMovies(input: [{ title: $t, released: 2030,
actors: { connect: [{ where: { node: { name: { eq: "Tom Hanks" } } }, edge: { roles: ["Agent test"] } }] } }]) {
movies { title }
}
}
Rules an agent must know (also in the server mutation guide):
- Update scalars with
{ set: value }. - Change relationships as a difference (connect what is new, disconnect what is gone), never “disconnect all then connect” in one mutation.
- Delete a movie with
deleteMovies(where: ...). Always filter;deleteMovieswith no filter deletes everything.
Cleanup of the example: mutation { deleteMovies(where: { title: { startsWith: "ZZ Agent" } }) { nodesDeleted } }.
On an open server nothing stops an agent from writing. That is why the MCP server and the example agent are read-only by default; see guardrails.
Source: grandstack-agents/docs/direct-graphql.md (this page is a copy; edit it in the repository)