Docs / Concepts
GraphQL APIs
How Sokie makes tools from a GraphQL schema: where the schema comes from, what each tool asks for, and what it can't do yet.
Sokie reads a GraphQL schema the way it reads an OpenAPI spec. Every field on Query and Mutation is an operation, each tool calls one of them, and the field's arguments become the tool's input. You never write a query: Sokie writes each tool's document when it builds the server, and an agent passes arguments and gets the field's data back.
Start in the app
sokie init reads OpenAPI and Swagger specs only, for now: given a .graphql file, it stops and says GraphQL comes later. Make a GraphQL server from New server in the app instead. It takes the schema three ways:
- From a URL: give the API's endpoint, such as
https://api.example.com/graphql. If the URL serves a schema file, Sokie reads that. Otherwise it sends the endpoint GraphQL's introspection query and uses the schema that comes back. If the endpoint wants a key even for that, tick It needs a header to read: the header goes with that read only, and is never stored. Sokie fetches over https, and never from an address inside a network. - Paste a spec, or drop a
.graphqlfile: SDL, which Sokie knows by itstype Query,type Mutationorschema { }, or a saved introspection result, which is JSON with a__schemain it. - A GitHub repository you've already linked to a server: its
.graphqland.gqlfiles are offered, and so are JSON files named like a spec, such asschema.json.
Sokie says what it found, as GraphQL (SDL) or GraphQL (introspection); if it guessed wrong, pick GraphQL under Spec type. If the API has introspection turned off, reading its endpoint fails; paste or drop its SDL instead.
A schema doesn't say where its API is served, so the page asks Where the API answers: the endpoint every tool calls, which becomes upstream.baseUrl in sokie.json. When the schema came from introspection, the field is already filled in with that endpoint.
The schema is committed with the server's files, as schema.graphql for SDL or schema.json for an introspection result. A dropped file, or one from a repository, keeps its own name. A build reads only that copy, so when the API's schema changes, update the file.
Which fields become tools
Every field on Query and Mutation becomes an operation, which a tool file names as query <field> or mutation <field>. Subscriptions aren't read. Take this schema:
scalar DateTime
type User { id: ID! name: String }
type Project { id: ID! name: String! owner: User! }
type Comment { id: ID! body: String! author: User! }
type Task {
id: ID!
title: String!
done: Boolean!
due: DateTime
project: Project!
comments(first: Int): [Comment!]!
}
input NewTask { title: String! projectId: ID! due: DateTime }
type Query {
"Tasks in a project, newest first."
tasks(projectId: ID!, first: Int, done: Boolean): [Task!]!
}
type Mutation {
createTask(input: NewTask!): Task!
deleteTask(id: ID!): Boolean!
}It has three operations, and the new server gets a file for each, named for its field: tools/tasks.md, tools/createTask.md and tools/deleteTask.md.
--- operation: query tasks risk: read --- Tasks in a project, newest first.
A field's description in the schema becomes the tool's description, and each argument's description goes into its input. A field with none gets Calls mutation createTask., which sokie check flags as too thin for an agent to choose from. Make tools easy for agents to use says what to write instead.
To keep a field from agents, leave it out on the New server page, or delete its file later. A GraphQL schema has no x-mcp: false, so sokie check lists fields without a tool as a note, and sokie tools --unexposed names the file each one would take.
Arguments become the input
Each argument is a property of the tool's input, and an argument marked ! is required. Arguments reach the API as GraphQL variables; one the agent leaves out isn't sent, so the API's default applies. Types map like this:
StringandIDare strings,Intan integer,Floata number andBooleana boolean.Date,DateTime,UUIDandURLare strings with that format.- An enum is a string limited to its values. A list is an array.
- An input type is an object with its own required fields, and no others allowed.
- Any other custom scalar is a string, and
sokie checkwarns. So does an input type that contains itself, which accepts any object where it repeats.
What each tool asks for
Sokie writes each tool's document when it builds the server. For tasks above, it's:
query QueryTasks($projectId: ID!, $first: Int, $done: Boolean) {
tasks(projectId: $projectId, first: $first, done: $done) {
id
title
done
due
project {
id
name
}
comments(first: 10) {
id
body
}
}
}The selection follows a few rules, so the document is always one the schema accepts:
- It takes every field of the type the operation returns, and of the objects under it, four levels deep. Past that, and at any type already on the way down, it asks only for
__typename, andsokie checkwarns. - A field with a required argument is left out, since the tool has nothing to pass it. A field with an optional
firstorlast, likecommentsabove, is asked for its first 10 items: APIs like GitHub's refuse a connection asked for without one, though their schemas say both are optional. Any other field whose arguments are all optional is asked for without them. - Inside nested objects, a field that returns one non-null object, like
owner: User!, is left out. If the API returns null there anyway, GraphQL throws away the whole response. - For a union or an interface, it asks for
__typenameand each concrete type's fields, for up to eight types. A field two types declare differently is aliased per type, asIssue_state: state. - The operation is named for its field,
QueryTasksorMutationCreateTask, so the API's own logs show which tool called.
The selection can't be changed in a tool file. A tool returns the field's own value, data.tasks here, not the whole response. To hand the agent less, add a JMESPath project: to the tool's file; it applies to that value, so project: "[].{id: id, title: title, done: done}" keeps three fields of each task.
If the field came back, the tool returns it, and if the response also lists errors, their messages come after the data, so the agent knows part of it failed. If the field came back null with errors, the call fails with the API's error messages, and an HTTP error status fails it too.
Set each tool's risk
A query is a read, so its file starts with risk: read. A mutation starts with no risk: write, its floor, applies, and sokie check warns until you set one. Raise it where it fits: delete for a mutation that removes something, external_effect for one that acts outside the API, like sending an email. A risk can't go below the floor, so a mutation is never a read.
--- operation: mutation deleteTask risk: delete idempotent: true --- Delete a task and its comments by the task's id. It can't be undone, so check the id with `tasks` first.
Clients see a read as read-only and safe to retry. A mutation isn't safe to retry unless its file says idempotent: true, and delete and external_effect are marked destructive. Left at write, deleteTask gets an effect-understated warning from sokie check, since its name says delete. On the New server page, you can set each tool's effect before anything is made.
Give the API its credentials
A schema can't say how its API signs in, so a new GraphQL server sends no credentials, and the header you gave to read the schema isn't kept. Add upstream.auth to sokie.json. For a bearer token that each person who connects brings for themselves:
{
"upstream": {
"baseUrl": "https://api.example.com/graphql",
"auth": {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"prefix": "Bearer ",
"key": "perCaller"
}
}
}For one key that serves every caller, make key a secret, like {"secret": "TASKS_TOKEN"}, declare it under secrets and set it with sokie secrets set (Add secrets). OAuth to the API, and fixed headers under upstream.headers, work as they do for any server; How clients sign in covers each.
Every GraphQL tool sends the credential, and a server that uses each caller's own key must be private. Sokie asks a caller for a new key when the API answers 401, or when it reports the key as refused inside a 200, with an error code like UNAUTHENTICATED or invalid_token, as many GraphQL APIs do.
To change sokie.json on a server you made in the app, choose Download files on its Tools tab, edit the file and run sokie deploy --server <slug>, or link a repository and push.
Large schemas
If you keep more than 40 tools, the New server page sets "discovery": "search", and clients get three tools, searchTools, getToolDefinition and useTool, instead of the whole list. Many operations explains how search ranks. A GraphQL tool without tags is tagged with its operation type and field, so a search for mutation finds the mutations.
Limits
sokie initdoesn't read GraphQL yet; start in the app.- Subscriptions aren't tools. A tool file that names one is an
unknown-operationerror. - Each tool calls one root field, with the document Sokie writes. You can't write your own query, add fragments or pick fields.
- Every call is a JSON
POSTofqueryandvariablestoupstream.baseUrl, so multipart file uploads don't work: anUploadargument is a custom scalar, sent as a string. - An SDL file must define every directive it uses. One that uses federation's
@keywithout defining it doesn't parse; read the schema by introspection instead. - A schema as SDL can be up to 2 MiB, and as introspection JSON up to 14 MiB.