How a tool is built
-
One file makes one tool. An operation file is a
.graphqlfile that holds one GraphQL operation and the fragments it spreads. A file that holds a second operation stops startup. A tool call returns one result. A file that holds a subscription, or that uses@deferor@stream, therefore stops startup as well. -
The name comes from the operation name through a naming strategy. A naming strategy is the rule that turns an operation name into a tool name, and
camel-caseis the default.@gatool(name: "movies_top_rated")sets an explicit name. AToolNamingStrategybean replaces the strategy. An operation written without a name takes its name from the file, sotop-rated-movies.graphqlandTopRatedMovies.graphqlboth givetopRatedMovies. Under theas-writtenstrategy, the file's own case is kept and a run of separators becomes one underscore, so the first file givestop_rated_movies. GATool refuses an operation without a name when the file name holds a letter outside ASCII, such asüber-filme.graphql. The refusal happens on every platform. It does not depend on whether the file system hands the name over composed or decomposed. Composed means one code point forü, and decomposed meansuplus a combining mark. Name the operation in the file, and the file keeps its name. An explicit name follows the same rules as a name from a strategy. It uses letters, digits,_and-, with at most 64 characters. It holds at least one letter or digit. -
The description comes from the comment directly above the operation. Without one, it comes from the schema description of the root field the operation selects. Startup warns when both are silent. A comment or a schema description made of characters that render empty, such as a no-break space, counts as silent. The schema may mark that root field
@deprecated. The description then ends with the deprecation and the schema's reason, as inLists every movie. Deprecated: Use movies.. The model reads that the API is removing the field the tool calls. The sentence is added after a comment and after a schema description alike. A generated tool, one GATool writes from a root field undergatool.dev.experimental.generate-tools, carries the same sentence. A deprecated field deeper in the selection only reaches the startup warning. The same holds for a deprecated root field when the operation selects several root fields. The September 2025 edition of GraphQL allows a description string ahead of the operation. GATool refuses that syntax at startup, because graphql-java 25.0 predates it. The refusal says so and points at the#comment lines. -
The input schema is the JSON Schema that describes the arguments of a tool. It comes from the variables, with their types, their descriptions and their defaults. An
IDargument accepts a string or an integer, which is what GraphQL's own input coercion takes. AnIDin a result is a string, which is what a conforming service serializes. A default is published as the JSON Schemadefaultkeyword, coerced the way GraphQL coerces it. The model therefore reads what it gets by leaving the argument out. A whole number keeps every digit the file wrote. ALongdefault past 64 bits therefore publishes as those digits. A default that fails the schema of its own property is left out, and startup names it.@oneOfmarks an input object where exactly one field is set. A nullable variable may fill a@oneOfinput field through a nullable argument or input field. GATool refuses that variable, even when it has a default. Declare it non-null. Section 5.8.5 of the September 2025 specification allows that default. graphql-js 17 refuses it in that position. GATool follows graphql-js. -
The title comes from
@gatool(title: "Top rated movies"). A blank title is refused. A blank title is one that is empty, or one made of spaces. Characters that render empty, such as a no-break space or a zero width space, count as spaces here. -
The open world hint comes from
@gatool(openWorld: true)or@gatool(openWorld: false). It setsopenWorldHinton the tool definition of the Model Context Protocol (MCP). The hint says whether the tool reaches an open-ended set of entities. A file that leaves it out publishes the tool without the hint. -
The result is the
dataanderrorsof the GraphQL response as JSON text. GATool writes that text itself. The response-levelextensionsand every other top-level key stay out. An error's ownextensionstravel with the error. A294 Partial Success, or any 2xx status under a JSON content type, is read the same way as a200. A GraphQL error becomes a tool error that the model can read and act on. -
The hints
readOnlyHint,destructiveHintandidempotentHintare set on every tool. A query publishesreadOnlyHintandidempotentHintas true anddestructiveHintas false. A mutation publishes the opposite three values. A mutation file becomes a tool the same way a query file does. The operation type tells a client which it is. -
The output schema is the JSON Schema that describes the result of a tool. It is off by default.
gatool.results.publish-output-schema: trueturns it on for every tool.@gatool(outputSchema: true)or@gatool(outputSchema: false)overrides that for one operation either way. An application can therefore publish for the tools it trusts and hold back for one awkward operation.
Every @gatool problem names the line and column of the argument it is about.
An empty or comment-only operation file is reported as empty. An operation
file, a shared fragment file and the schema file may end each line with \n,
\r\n or a lone \r. GATool reads the three forms the same way. A block
string, a GraphQL string written between triple quotes, therefore holds one
value on a Windows checkout and on a Linux one. A tool publishes one contract
on both. The three files are the operator's own text, so GATool reads them at
any length. "The size of a document" below says which limits remain and which
the API sets.
gatool-core/src/main/resources/io/gatool/core/gatool-directives.graphqls
holds the @gatool directive in SDL, the GraphQL schema definition language.
It exists for an editor's completion and for a schema linter. GATool reads the
directive from the operation file itself. The SDL file serves editors alone.
What the input schema checks
The MCP SDK validates the arguments of a call against the published input schema before the tool runs. The GraphQL API then coerces the values that reach it. The table lists the positions where the two answer differently for one value:
| Position | What the schema does | What the API does |
|---|---|---|
A Non-Null custom scalar published as {} | Accepts null. The empty schema leaves every type open, null included. The variable stays in required, so a call that leaves it out is refused | Refuses the null with a request error. The model reads it as the tool error |
A list, such as [Int!]! | Refuses the single value 1999, because the property says array | Takes 1999 as [1999], which is GraphQL's input coercion for a list. The model sends the list, because the schema is what it reads |
| An input type inside another of its own kind, and one past the node budget | Accepts a field the type lacks. The object there is published with a description and without its fields | Refuses the field with a request error. The model reads it as the tool error |
A custom scalar is a scalar type that the schema defines itself. It is
published as {} until a scalar fragment, or a specification GATool has read,
describes it. A scalar fragment is the piece of JSON Schema configured under
gatool.inputs.scalar-schemas for one scalar. A scalar fragment that names a
type closes the position, so the schema states a type there. The Non-Null
variable then publishes that type without a null branch. "Custom scalars" below
shows how to write a scalar fragment. The third row covers two cases. The first
is a type that holds another of its own kind, such as a filter with and and
or. That type lists its fields and closes the object where it first appears.
The copy nested inside it is published open. The second is a position past the
node budget. One variable expands at most 500 input objects. A position past
that budget is published open as well, with a startup warning that names it. An
in-process tool sends its arguments without this validation, so the API answers
for every row there.
A variable typed as a generated filter costs more than its one line in the
operation suggests. A generated filter is the kind of input type that Hasura
and PostGraphile publish. One where variable over eight tables with three
relationships publishes an input schema of about 100,000 characters.
tools/list carries that schema to every client. Startup warns about a tool
above 4,000 estimated tokens and names it. To avoid this, declare the scalar
variables the tool needs and write the filter as a literal in the operation.
The budget of 500 input objects and the text published past it may change in a
later release.
The schemas GATool writes are built for function calling without OpenAI's
strict mode. Two positions fail OpenAI's rules for a tool under strict: true.
The first is an optional property, because strict mode requires every property
in required. The second is a custom scalar published as {}, because strict
mode requires a type on every property. A scalar fragment under
gatool.inputs.scalar-schemas closes the second position. The first stays open
for a later release.
What the output schema promises
In MCP, an output schema is optional. Once a server publishes one, it is
binding: every call must return structured content that conforms to it.
Structured content is the JSON result an MCP tool returns beside its text.
GATool's output schema describes the GraphQL response envelope, the object that
holds data and errors. GATool reads the structured content back from the
text it wrote. The two halves therefore carry the same digits and nulls. That
holds in embedded mode, where the GraphQL API runs inside the same application
through Spring for GraphQL. It holds for a remote API as well. Where the
selection set fails to prove a shape, the schema stays permissive:
| Case | What the schema says |
|---|---|
data | Nullable, because a failed call answers with data: null |
| Any selected field | Present in required, and its value nullable |
A field under @skip or @include | Out of required, since the model decides whether it arrives |
| A field inside a fragment with a type condition | Out of required, since another concrete type leaves it out. A condition that names the surrounding type, an interface it implements or a union holding it keeps its fields in required. Such a condition holds for every object that can answer there |
| An enum | string, without the value list. A value the API adds before the schema file catches up still conforms |
| A union or an interface | One branch per member the operation's type conditions cover, plus an open branch and null |
| A custom scalar | The type and the format of its scalar fragment under gatool.inputs.scalar-schemas, or its entry under gatool.results.scalar-schemas as written. A scalar without an entry in either map may cite a specification through its @specifiedBy URL. Where GATool has read that specification, the schema publishes its type and format. Otherwise the schema is empty and accepts any JSON value |
errors | The GraphQL shape: an array of objects whose message, locations, path and extensions may each be null or left out. Then null. Then an open branch that accepts any JSON value. An API, or a gateway in front of it, may write other shapes, such as an entry without message, an entry that is a plain string, or one object where the list belongs. Each of them conforms |
A response that carries errors beside its data conforms as well, so it arrives
with both halves. The tool error flag says the response carries errors. The
structured content says what came back with them. Errors in a shape other than
the GraphQL one count as errors for that flag. The text and the structured
content carry them as the API wrote them. GATool writes some refusals itself,
such as a result above one of the two response caps,
gatool.api.max-response-size and gatool.results.max-characters. Such a
refusal carries a sentence in place of the envelope and goes back as text
alone.
Structured content reaches an MCP client alone. Spring AI's in-process tools
carry text, so the output schema does not change what a ChatClient receives.
Unions, and the errors-as-data pattern
The errors-as-data pattern puts an expected failure, such as a declined card,
into the result type. The failure is a union member beside the success type. A
union or interface position publishes one branch per member its type conditions
cover. __typename is the field that names the concrete type of an object in
the response. When the operation selects it, each branch pins its value. This
operation:
query Pay($cardId: ID!) {
pay(cardId: $cardId) {
__typename
... on Payment { id amountCents }
... on InsufficientFunds { message shortfallCents }
... on CardDeclined { message code }
}
}
publishes a Payment branch keyed {"const": "Payment"}, one branch for each
error member, then an open {"type": "object"} branch and {"type": "null"}.
Both error members carry message. The pinned __typename lets the model tell
them apart, and it is what the model switches on.
With the open branch present, the position only enforces "an object, or null". The member branches exist for the model to read. The open branch keeps the schema safe. The API may add a member after the operation file was written. A result with that member lands in the open branch. Without the open branch, the call would fail on an operation file that had not changed.
An operation that leaves __typename out gets the same branches without the
const. Startup warns, because the model then cannot tell from the response
which member it holds. GATool only removes its own directive from a document.
It does not add __typename to your query. A type condition may name the
position's own type, or an interface that type declares. Such a condition adds
its fields to the one shape every member shares. It does not get a branch of
its own. A fragment on Node at a node position therefore publishes one
shape, and startup stays quiet. One output schema holds at most 500 object
shapes. A position past that bound is published as an open object. A result
there still conforms, and startup names the position.
Custom scalars
A custom scalar reaches the model as {}, the JSON Schema that accepts any
value. The model then guesses the format and learns it from the API's request
error. Two things narrow that: a @specifiedBy URL that GATool has read, and a
scalar fragment you write.
A @specifiedBy URL GATool has read. @specifiedBy is the directive a
schema puts on a scalar to point at the specification of its values. GATool has
read every specification the community registry publishes, 31 in all. It has
also read RFC 4122 and RFC 3986, which the GraphQL specification uses in its own
examples. Nine of them carry a JSON Schema format. The rest describe the shape
in words, because a format would refuse a value the API takes. GATool
normalises the URL before the lookup, so every spelling of one page finds one
entry. tools.ietf.org and datatracker.ietf.org both find an RFC, with or
without .html, and with or without a section anchor. The scalar keeps its own
description either way. Where the entry describes the shape in words, the
property carries the URL. The model can then see which page defines the value.
A scalar fragment you write. Most schemas leave @specifiedBy unset. A
scalar's name alone does not say which values the API takes. That rule lives in
the API's coercion code, which the schema leaves out. You state the format
yourself:
gatool:
inputs:
scalar-schemas:
DateTime:
type: string
format: date-time
description: An RFC 3339 timestamp, for example 2026-09-18T10:00:00Z.
Startup names every scalar that is still undescribed, so the warning tells you
which fragments to write. A scalar fragment wins over the @specifiedBy table.
Its description replaces the scalar's own.
Six keywords are published. Startup refuses any other keyword and names the reason:
| Keyword | What it does |
|---|---|
type | string, integer, number or boolean. It asserts, so a wrong one refuses a value your API would take. GATool writes the null branch itself |
format | One of the ten formats Anthropic documents: date-time, time, date, duration, email, hostname, uri, ipv4, ipv6 and uuid. It guides the model, and validation reads past it. OpenAI's strict subset leaves uri out, so a fragment that writes uri publishes with a startup warning |
pattern | A regular expression the string has to match. It asserts, so prefer one that over-accepts. A pattern longer than 200 characters is refused. The pattern stays inside the syntax Java and ECMA 262 share, because a client compiles it as ECMA 262. Startup refuses a Java-only construct by name: inline flags, \A, \z, \Z, \Q...\E, \p{...}, the escapes \R, \h, \H, \v, \V, \X, \G, \e, \a, \N{...} and \x{...}, && or a nested [...] inside a class, possessive quantifiers and atomic groups. Startup also refuses lookaround, a backreference, a named group and \b, because Anthropic's strict tool use answers them with 400 Invalid regex in pattern field |
enum | A closed value set, which only the API's owner knows. It asserts, because the MCP SDK's validator enforces it. An enum of more than 250 values is refused |
description | The field that helps the model most, and the replacement for every refused keyword. A description longer than 1,024 characters is refused |
anyOf | Two to eight branches, for a scalar with more than one wire form |
A scalar fragment whose JSON is longer than 2,048 characters is refused as a
whole. The schema travels in every tools/list and in every prompt that carries
the tool, so it has to stay short.
A numeric bound is refused, because Anthropic's strict tool use answers
minimum with HTTP 400: For 'integer' type, properties maximum, minimum are not supported. oneOf is refused the same way. State a bound in description.
Anthropic's own SDKs do the same.
Three of the six keywords assert, so a scalar fragment can refuse a call before
the API sees it. They are type, pattern and enum, each measured against
the MCP SDK's own validator. You write the fragment because only you know the
wire form. Your Long may travel as a number, as a quoted string, or as either.
Write the wider shape when you are unsure, and let the API answer the rest.
A profile adds to a scalar fragment. gatool.inputs.scalar-schemas is a map
of maps, and Spring Boot merges a map across property sources. A fragment
written in application.yml and again in application-prod.yml therefore
publishes the keywords of both files. The profile changes the value of a
keyword the base file wrote, and it adds keywords of its own. A keyword the base
file wrote stays in the fragment when the profile leaves it out. Take a base
fragment that holds a pattern for a date. A profile writes type: string and
format: date-time over it. The merged fragment publishes all three keywords.
The timestamp the description asks for then fails the date pattern. To avoid
this, keep a keyword that differs between environments out of the base file and
write it in each profile. Or write that keyword in the profile as well, with the
value the profile needs. gatool.results.scalar-schemas merges the same way.
A result reuses the scalar fragment. You write the fragment once. Where a
tool publishes an output schema, a result describes the scalar from the same
fragment: its type, its format and its description. For a fragment that is
an anyOf, a result takes the type and the format of each branch. GATool
writes null beside them, as it does for every value of a result. The
description is written at the first position of each output schema where the
scalar appears. The later positions leave it out, because the schema travels in
every tools/list.
enum and pattern apply to arguments alone. A scalar fragment says what the
model should send. You may write it narrower than the API on purpose. An enum
may list fewer values than the API holds. A pattern may ask for 2026-09-28
while the API returns 2026-09-28T00:00:00Z. An output schema is binding in
MCP. A result that fails it is reported as an error, after the API has already
run the call. For a mutation, that error invites the model to retry a write that
was already applied. type and format describe the wire form, which a scalar
normally shares in both directions.
One case needs a second entry: a scalar that returns another form than it
accepts. Take a Money that is accepted as a number and returned as a quoted
string. It fails the reused type: number on every result. An entry under
gatool.results.scalar-schemas states what a result carries:
gatool:
inputs:
scalar-schemas:
Money:
type: number
description: An amount with two decimals, for example 12.50.
results:
scalar-schemas:
Money:
type: string
description: An amount with two decimals, as a quoted string such as "12.50".
An entry there follows the rules of the table above. The output schema
publishes it as written, its enum and pattern included, because you wrote
them about results. It takes the place of the input fragment there. The input
schema reads gatool.inputs.scalar-schemas alone. To switch the reuse off for
one scalar, give it an entry under gatool.results.scalar-schemas that holds a
description alone. Its results then publish the empty schema, which accepts
any JSON value.
Take a scalar without a fragment whose @specifiedBy cites a specification
GATool has read. A result publishes the type and the format of that
specification. An argument publishes the same. A specification states one wire
form for both directions. A scalar fragment wins over the specification for
arguments and for results. An API that cites a specification and returns
another form takes an entry under gatool.results.scalar-schemas, like any
scalar that returns another form than it accepts.
A 64-bit integer needs both forms. A JSON reader that stores a number as a
double (IEEE 754 binary64) changes 9223372036854775807 into
9223372036854776000. The model can send the right digits and a client still
hands the server a different number:
gatool:
inputs:
scalar-schemas:
Long:
anyOf:
- type: integer
- type: string
pattern: "^-?(0|[1-9][0-9]{0,18})$"
description: >-
A 64-bit signed integer. Send a value outside -9007199254740991 to
9007199254740991 as a quoted string, for example "9223372036854775807".
Whether the string branch belongs there depends on the API. graphql-java's
extended Long takes both forms. Three different Long specifications are
published, and one of them takes the string alone. The same scalar name means
different things behind different APIs, so the fragment has to come from you.
A decimal reaches the API with every digit. GATool's own mapper reads a
float as a BigDecimal on every surface: a result, an in-process argument and
an MCP argument. You do not need
spring.jackson.deserialization.use-big-decimal-for-floats. The MCP transport
reads a request with the mcpServerJsonMapper bean. That bean enables the same
setting, USE_BIG_DECIMAL_FOR_FLOATS. The digits an API receives can differ
from what the model wrote, because a BigDecimal prints its own way. 1e2
reaches the API as 1E+2, and 12.50 reaches it as 12.50, both valid JSON
numbers. A decimal past the range of a double, such as 1E400, reaches the
API as a number.
Shared fragments
An operation file defines the fragments it spreads. A fragment is a named
selection set that an operation reuses with a spread, ...MovieCard. A fragment
that several operations share can live in a file of its own, for example under
gatool/mcp/fragments/. A file that holds fragments alone becomes a shared
fragment file for the locations that reach it. Startup attaches each shared
fragment to every operation that spreads it and validates the assembled
document. The fragments are appended in the order of the first spread, depth
first, so the text an API receives stays the same between builds.
-
A spread resolves to the operation file's own fragment first. It resolves to a shared fragment file in the same folder second, the MCP folder or the in-process folder. Two operation files may therefore each define their own
MovieCard. -
An operation file whose own fragment has the name of a shared one keeps its own, and startup warns naming both files.
-
Two shared fragment files in one folder that define the same name stop startup, and the message names both files.
-
A shared fragment left unspread by every operation file earns a warning naming its file. GATool still validates it on its own. A wrong field in it stops startup with the fragment file's line and column. A spread without a definition in either place stops startup like any other validation error.
-
@deferand@streaminside a shared fragment file stop startup, as they do in an operation file. The message names the fragment file. A validation error inside a shared fragment names the fragment file, with its own line and column, beside the operation file.
The CI check returns the assembled text of every tool. The startup listing is
the list of tools GATool prints at INFO when the application starts. For a tool
that uses shared fragments, it also prints the assembled text at DEBUG for
io.gatool. GATool does not support #import comments, because the GraphQL
specification treats comments like white space.
The size of a document
GATool limits what it reads from a file, and the API limits what it accepts in a request. The two are set in different places, so they can disagree.
What GATool reads. An operation file, a shared fragment file and the schema
file are your own text, so GATool reads them however long they are. One limit
remains on an operation file and a fragment file: selection sets may nest at
most 165 deep. A document nested far deeper exhausts the stack of the thread
that prints it. A file past that depth stops startup with a problem that names
the file. The problem says that graphql-java's parser stopped reading the file
at a limit. It quotes graphql-java's own count: More than 500 deep 'grammar' rules have been entered. Those 500 grammar rules come to 165
selection sets. The limit is fixed in this release, and a property cannot
change it.
What the API reads. The API applies limits of its own to each request, and
it refuses a call whose document passes them. GraphQL's introspection describes
types and leaves request limits out, so GATool cannot read them from the API.
You state them under gatool.api.request-limits. Startup and the CI check then
warn about a tool whose document passes one:
| Property | Default, which is what graphql-java reads of a request |
|---|---|
gatool.api.request-limits.max-characters | 1,048,576 |
gatool.api.request-limits.max-tokens | 15,000 |
gatool.api.request-limits.max-whitespace-tokens | 200,000 |
gatool:
api:
request-limits:
max-tokens: 5000
Set each to the limit of your API. The defaults hold for an API built on graphql-java that left its parser options alone. Apollo Router stops at 15,000 tokens by default as well. It counts the ignored tokens, such as commas, among them, so its limit is reached sooner. A value below 1 leaves that size unchecked. The properties decide what GATool warns about. The API keeps applying its own limits, whatever the properties say. A value above the API's limit therefore silences a warning about a call the API will refuse.
The document counted is the one GATool sends: the operation as graphql-java prints it, with the fragments of every shared file it spreads. The printer indents each field, so a deeply nested document gains whitespace the file did not hold. The tool is served either way. To get under the limit, select less or split the operation.
What the model writes. executeGraphql is one of the dynamic tools,
described under "Getting started without writing operation files". A document
the model writes for executeGraphql is read under graphql-java's limits for a
request, nesting included. It is also read under three limits you set:
max-depth, max-fields and max-aliases under
gatool.dev.experimental.dynamic-operations. They protect the API from a
document that reached it without review, so set them at or below the limits of
the API.
Reading the schema from a registry
gatool.api.schema.location is a Spring Resource. The value decides where
the schema comes from. classpath: and file: read a file in the jar or on a
mounted volume. GATool itself fetches an http or https URL at startup. A
registry serves the schema with a key in a header, and Spring's own
UrlResource cannot send one. Spring Cloud AWS, GCP and Azure register
s3://, gs:// and azure-blob:// with the same resource loader. A team adds
the matching starter and writes the URL.
GraphQL Hive
serves the SDL of a target from its CDN, with the key in X-Hive-CDN-Key:
gatool:
api:
schema:
location: https://cdn.graphql-hive.com/artifacts/v1/${HIVE_TARGET_ID}/sdl
header-name: X-Hive-CDN-Key
header-value: ${HIVE_CDN_KEY}
The key stays out of every log line. Startup names the URL without its userinfo
and query. A secret read from a file often ends in a line break. A value holding
a line break stops startup, and the message names the property. A header name
outside RFC 9110's token characters stops startup the same way. A space inside
the value, as in Bearer <key>, is accepted. GATool follows up to five
redirects itself. It sends the key to the configured origin alone. A Location
on the same scheme, host and port gets the key. Any other host is fetched
without it. The pre-signed storage URL that Hive's CDN answers with is one
example. A redirect loop or a sixth hop stops startup, and the message names the
URL. spring.http.clients.redirects does not apply to this request.
gatool.api.schema.max-size bounds the body, at 10MB by default. This bound is
separate from one of the two response caps of a tool call,
gatool.api.max-response-size. A schema larger than that cap is therefore
fetched, while the cap stays where the deployment set it. A schema above the
bound stops startup, and the message names the property. The application may
leave spring.http.clients.connect-timeout or read-timeout unset. GATool then
fills the missing one: 3 seconds to connect, 15 seconds for an answer. Startup
says so at INFO, as it does for the API. Hive's CDN answers with an ETag.
GATool keeps it on the first line of the cached copy under
gatool.api.schema.cache-directory. The default is schema, inside a folder
named gatool- and the account the JVM runs as, under the JVM's temporary
directory (java.io.tmpdir). GATool writes the copy only after every operation
file has validated against the fetched schema. The copy therefore holds the last
schema that validated. A fetch that breaks an operation stops startup and leaves
the copy unchanged. The next startup sends If-None-Match. A 304 reuses the
copy. A registry that cannot be reached, or that answers 5xx, starts the
application on the copy. A warning then names the copy's age. A 4xx such as
401 stops startup even with a copy on disk. The message says that the server
answered 401 UNAUTHORIZED. Spring Boot's failure analysis adds the property
name, gatool.api.schema.location. The directory is configurable. A team that
wants the copy to outlive the temporary directory sets the property to a path of
its own. A persistent volume is one such path. A blank value fetches at every
startup and stops when the fetch fails.
The default folder is checked before it is used. Every account on a Linux host shares the temporary directory. Another account could create GATool's folder first and put a schema into it. The descriptions of that schema would then reach the model as tool text. GATool makes the folder for its owner alone. It uses the folder only while the account it runs as owns it, and while group and others cannot write to it. A folder that fails the check is left unread and unwritten, and startup warns with the reason. The schema is then fetched at every startup. A startup while the registry is down stops. A directory you configure is used as it is. A volume mounted into a container is often owned by root and writable by a group, and it is yours to secure.
Apollo GraphOS and WunderGraph Cosmo hand the schema to their own command line
tools. rover supergraph fetch and rover graph fetch write the supergraph
or the API schema. wgc federated-graph fetch-schema writes the router schema
of a federated graph, or the client schema with --client-schema. Package the
API schema or the client schema. A supergraph or router schema carries fields
marked @inaccessible. Those fields pass validation at startup, and the router
refuses them at call time. GATool reads the file as written. A later release
may derive the API schema itself. A build step that runs one of these tools
and packages the file gives GATool a classpath: or file: location. A team
that publishes the file to a bucket or behind a URL of its own uses the
protocols above.
Getting started without writing operation files
A development switch gives you tools from the schema alone. It is off by default. Startup warns on every boot while it is on:
gatool:
dev:
experimental:
generate-tools: all-root-queries # or all-root-queries-and-mutations
generated-selection-depth: 1 # 1 to 5
generate-tools-for-deprecated-root-fields: true
generated-operations-include-deprecated-fields: true
Every root field becomes one tool. The field name becomes the operation name,
and the tool naming strategy turns that into the tool name. The default
strategy, camel-case under gatool.naming.strategy, leaves a camelCase
field name as it is. The field's arguments become the tool's arguments with
their defaults. The field's own schema description becomes the tool
description. The selection set takes the leaves of the type the field returns.
Depth 1 therefore selects the scalars one level in. A wrapper costs one level
per hop. A Relay connection therefore needs 3 to reach node data: the
connection, its edges, then the node. At 2 you get cursors and page counts,
and the node data stays out.
A union selects __typename and one inline fragment per member. An interface
selects __typename and its own fields.
The generator skips two kinds of root field. The first is a field whose type expands to an empty selection within the depth. The second is a field whose generated operation is longer than 20,000 characters. Startup names each skipped field. For the first kind it says whether a deeper setting would reach it. For the second kind it tells you to lower the depth. Inside a selection, a field that would reach a type already on the path is left out. The rest of the selection stays. A required argument of a root field becomes a required argument of the tool. A field inside the selection that demands an argument is left out of the selection, because the generator cannot supply its value. A root field you dropped with the deprecation property below is skipped without a message, since you asked for it.
Deprecated fields stay in until you say otherwise. Both deprecation
properties default to true, which is what the generator has always done. A
root field your schema marks @deprecated then becomes a tool. Its
deprecation goes into the tool description, where the model reads it. A
deprecated field takes its place in a generated selection set. Set
generate-tools-for-deprecated-root-fields to false, and that root field is
left out of the generation entirely. Set
generated-operations-include-deprecated-fields to false, and a deprecated
field stays out of a generated selection. The generator judges this by the
declaration of the type being selected. That setting is one way to trim what a
tool brings back. A root field whose type offers deprecated fields alone then
has an empty selection set and loses its tool. Startup says which setting
reaches it: the depth, this property, or only both together.
An operation file wins. A root field your files already select keeps your operation. You can therefore generate the lot, pick the few you want, and write those as files. The rest stay generated.
Why it lives under dev.experimental. GATool exists to serve trusted
documents, the operation files checked into your repository. The operation
files state which operations exist. A pull request reviews each one. The model
chooses from that list. The generator publishes the whole root of your API to
a model. Trusted documents exist to prevent exactly that. The generator may
change in any release. Startup says so on every boot while it is on. The
generated text goes through the same parser, validator and catalog as a file
you wrote. A generated tool therefore behaves as a tool from a file does.
The dynamic tools need two dependencies. With
gatool.dev.experimental.generate-tools set to dynamic-three-step, GATool
publishes searchSchema, introspectType and executeGraphql, the dynamic
tools. The search ranks schema coordinates with Lucene. A schema coordinate is
a type name, or a type and field name, such as Query.topRatedMovies. Neither
starter carries Lucene, because the dynamic tools are off by default. The two
jars are also 6.3 MB together (lucene-core 4.6 MB, lucene-analysis-common
1.7 MB). Add both yourself:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>10.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analysis-common</artifactId>
<version>10.3.2</version>
</dependency>
GATool builds against 10.3.2. Startup stops and names each missing jar when the
dynamic tools are on and one of the two is absent. The lucene-core jar holds the
index. The lucene-analysis-common jar splits an identifier such as
topRatedMovies into words and stems them. The split lets a question in plain
words find a field on a schema without descriptions. An application that
publishes a SchemaSearch bean of its own ranks with that bean, and both jars
stay out of its build. The bean builds its entries with SchemaCorpus.of, from
a schema the application reads on its own. GAToolCatalog holds the tools
alone, without the schema. The catalog depends on the search bean. A
SchemaSearch bean that injects the catalog therefore makes a cycle, which
Spring reports at startup. An operation file whose tool would take one of the
three names stops startup. The message names the file and the property. Give
that operation another name with @gatool(name:). A search hit on a field an
interface declares says so. It names an implementation on which to select the
field. introspectType lists an interface's implementations in a trailing
comment. Its description names the query root. It names the mutation root as
well while allow-mutations is on.
gatool.dev.experimental.dynamic-operations.include-deprecated-fields=false
hides deprecated fields, arguments, enum values and input fields from
searchSchema and introspectType. A hidden field still executes when the
model writes it into a document. The property saves tokens. It does not
control access. A field a caller must not reach belongs outside the schema the
API serves. A federated subgraph publishes Query._service { sdl }. A model
can read the whole SDL through executeGraphql with it. The dynamic tools
therefore publish what the API publishes, with one exception. executeGraphql
refuses a document that selects __schema or __type at any depth. The
refusal sends the model to the two schema tools. The three limits on the
document the model sends, max-depth, max-fields and max-aliases, count
the document's syntax. Argument values, list sizes and resolver cost stay with
the API's own demand control. The two response caps,
gatool.api.max-response-size and gatool.results.max-characters, apply after
the API computed the result.