Skip to main content

How a tool is built

  • One file makes one tool. An operation file is a .graphql file 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 @defer or @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-case is the default. @gatool(name: "movies_top_rated") sets an explicit name. A ToolNamingStrategy bean replaces the strategy. An operation written without a name takes its name from the file, so top-rated-movies.graphql and TopRatedMovies.graphql both give topRatedMovies. Under the as-written strategy, the file's own case is kept and a run of separators becomes one underscore, so the first file gives top_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 means u plus 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 in Lists 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 under gatool.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 ID argument accepts a string or an integer, which is what GraphQL's own input coercion takes. An ID in a result is a string, which is what a conforming service serializes. A default is published as the JSON Schema default keyword, 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. A Long default 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. @oneOf marks an input object where exactly one field is set. A nullable variable may fill a @oneOf input 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 sets openWorldHint on 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 data and errors of the GraphQL response as JSON text. GATool writes that text itself. The response-level extensions and every other top-level key stay out. An error's own extensions travel with the error. A 294 Partial Success, or any 2xx status under a JSON content type, is read the same way as a 200. A GraphQL error becomes a tool error that the model can read and act on.

  • The hints readOnlyHint, destructiveHint and idempotentHint are set on every tool. A query publishes readOnlyHint and idempotentHint as true and destructiveHint as 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: true turns 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:

PositionWhat the schema doesWhat 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 refusedRefuses 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 arrayTakes 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 budgetAccepts a field the type lacks. The object there is published with a description and without its fieldsRefuses 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:

CaseWhat the schema says
dataNullable, because a failed call answers with data: null
Any selected fieldPresent in required, and its value nullable
A field under @skip or @includeOut of required, since the model decides whether it arrives
A field inside a fragment with a type conditionOut 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 enumstring, without the value list. A value the API adds before the schema file catches up still conforms
A union or an interfaceOne branch per member the operation's type conditions cover, plus an open branch and null
A custom scalarThe 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
errorsThe 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:

KeywordWhat it does
typestring, integer, number or boolean. It asserts, so a wrong one refuses a value your API would take. GATool writes the null branch itself
formatOne 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
patternA 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
enumA 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
descriptionThe field that helps the model most, and the replacement for every refused keyword. A description longer than 1,024 characters is refused
anyOfTwo 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.

  • @defer and @stream inside 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:

PropertyDefault, which is what graphql-java reads of a request
gatool.api.request-limits.max-characters1,048,576
gatool.api.request-limits.max-tokens15,000
gatool.api.request-limits.max-whitespace-tokens200,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.