Configuration
| Property | What it does |
|---|---|
gatool.api.url | The GraphQL API every document goes to. GATool reports a redirect with its status and leaves it unfollowed, so name the URL the API answers at |
gatool.api.schema.location | The schema that operations validate against: a classpath: or file: resource, an https:// URL, or a protocol a Spring Cloud module registers |
gatool.api.schema.header-name | The header that carries a registry key when the location is an http or https URL |
gatool.api.schema.header-value | Its value, kept in the environment |
gatool.api.schema.cache-directory | Where a fetched schema and its ETag are kept. The default is schema inside gatool-<account> under the JVM's temporary directory (java.io.tmpdir), where <account> is the account the JVM runs as. GATool makes the default folder for its owner alone. It uses that folder only while the account owns it and group and others cannot write to it. A path of your own keeps the copy on a persistent volume and is used as it is. A blank value fetches the schema at every startup |
gatool.api.credentials.strategy | How GATool authenticates to that API. Unset sends each call without a credential. static-header sends the header below. client-credentials obtains GATool's own token. token-exchange exchanges the caller's |
gatool.api.credentials.header-name | The header that carries the credential, such as Authorization or X-API-Key |
gatool.api.credentials.header-value | Its value, such as a bearer token, kept in the environment |
gatool.api.credentials.client-registration-id | The Spring Boot client registration the two OAuth2 strategies authenticate through |
gatool.api.credentials.audience | The audience token exchange asks the issuer for, which the API validates |
gatool.api.credentials.resource | The resource token exchange names to the issuer, sent when set |
gatool.mcp.operations.locations | Where the MCP folder lives, the operation files served over the Model Context Protocol (MCP). The default is optional:classpath*:gatool/mcp/. Naming a folder replaces that default, so list the default again beside a new folder to keep the tools already there. A classpath*: location inside a jar needs directory entries in that jar, which Maven and Gradle write. Spring lists a jar's folder through those entries |
gatool.in-process.operations.locations | Where the in-process folder lives, optional:classpath*:gatool/in-process/ by default. It is replaced the same way, and the same jar rule applies |
gatool.naming.strategy | camel-case, snake-case or as-written |
gatool.results.max-characters | Largest result a tool returns, 60000 characters by default. The count is Java's String.length(), which counts UTF-16 code units. A character outside the Basic Multilingual Plane, such as an emoji, counts as two |
gatool.results.publish-output-schema | Whether tools publish an output schema and structured content. The dynamic tools, the ones dynamic-three-step generates, are published without one either way |
gatool.results.partial-results-as-success | Whether a response holding both data and errors counts as a success, off by default |
gatool.results.scalar-schemas.<Name> | The scalar fragment, a piece of JSON Schema, that the output schema publishes for a custom scalar that returns another form than it accepts. It replaces what the output schema reuses from gatool.inputs.scalar-schemas.<Name> |
gatool.api.request-limits.max-characters | Characters you expect the API to read of a document, 1,048,576 by default, which is what graphql-java reads of a request. Startup and the CI check, the OperationFilesAssert test, warn about a tool whose document holds more. The tool is served all the same. A value below 1 leaves the size unchecked |
gatool.api.request-limits.max-tokens | Tokens you expect the API to read of a document, 15,000 by default. The warning and the value below 1 work as for the characters |
gatool.api.request-limits.max-whitespace-tokens | Whitespace tokens you expect the API to read of a document, 200,000 by default, counted in the printed document GATool sends. The warning and the value below 1 work as for the characters |
gatool.api.max-response-size | Largest response GATool reads from the API, 1MB by default. This and gatool.results.max-characters are the two response caps. The value decides the heap one call in flight holds, which "Running in a container" sizes. A value in bytes below gatool.results.max-characters makes this cap refuse every result the result limit would have allowed. Startup then warns naming both |
gatool.api.schema.max-size | Largest schema GATool fetches from a URL, 10MB by default. A schema above it stops startup naming the property. The bound is the schema's own. A public schema of a few megabytes is fetched while gatool.api.max-response-size stays where the deployment set it |
gatool.mcp.rate-limit.calls-per-minute | Calls one caller makes to one tool each minute, 60 by default. Once the endpoint is secured, the caller is the token's subject. Where an application's own converter leaves the name blank, the caller is the token's fingerprint, a SHA-256 hash of the bearer token. The session binding, which ties an MCP session to the caller that opened it, uses the same key. While the unauthenticated switch is on, the caller is the client address. The caller is read through the SecurityContextHolderStrategy bean an application declares, so a strategy of the application's own keeps the count per caller. Behind a proxy that address is the proxy's. Set server.forward-headers-strategy=native with server.tomcat.remoteip.internal-proxies naming the proxy. That replaces a default which covers every private address range. framework trusts the forwarded headers every client sends, so a caller could then send a different address on every call and escape the count |
gatool.mcp.rate-limit.max-tracked-pairs | Most caller and tool pairs the limiter counts at once, 100,000 by default. Each pair holds a bucket of a few hundred bytes for a minute after its last call. Above the cap, the limiter drops a live pair's bucket for each new one, and that caller starts with a fresh allowance. The Micrometer counter gatool.rate-limit.evictions counts those drops. The log warns once a minute naming the property. Set it above the pairs active within a minute |
gatool.mcp.transport.max-request-body-size | Largest request body the MCP endpoint reads, 256KB by default |
gatool.mcp.transport.allowed-origins | The origins the endpoint accepts in an Origin header, empty by default. A request carrying the header is refused with 403 until its origin is listed. A request without the header is accepted. Each entry is a serialized origin, scheme://host[:port]. The scheme and the host are compared case-insensitively, and one trailing slash is ignored. An entry of another shape stops startup. CORS stays the application's. A browser client also needs a CorsConfigurationSource bean of the application's own, such as a UrlBasedCorsConfigurationSource. That bean is what Spring Security's cors() reads |
gatool.mcp.sessions.max-count | Most stateful sessions the server keeps at once, 1000 by default |
gatool.mcp.sessions.max-count-per-caller | Most stateful sessions one caller may hold at once, 100 by default. Zero or below turns it off |
gatool.mcp.sessions.idle-timeout | How long a stateful session may stay idle before eviction, 30 minutes by default. spring.ai.mcp.server.streamable-http.keep-alive-interval has to be shorter than it. Otherwise startup stops naming both |
gatool.mcp.security.baseline-scopes | Scopes every MCP call needs, published in the metadata and every 401 |
gatool.mcp.security.resource | The canonical resource URI the metadata publishes. Unset publishes the request's URL |
gatool.mcp.stdio.granted-scopes | The scopes the process that started a stdio server holds. Tools requiring scopes are checked against them |
gatool.mcp.stdio.log-to-stderr | Whether a stdio server writes its log to stderr, on by default. stderr carries WARN and above until logging.threshold.console sets another threshold. The log is written without making the server wait. A host that leaves stderr unread therefore loses lines while the server keeps answering, and a WARN line says how many once lines get through again. This applies on Logback, while the application leaves logging.console.enabled unset and the logging configuration to Spring Boot's defaults. On Log4j2, startup says in one line on stderr that the log is lost, while the application leaves the log file and logging.config unset. On java.util.logging, a log file leaves the stderr log on, and the application's own logging.config is what changes it |
gatool.inputs.scalar-schemas.<Name> | The scalar fragment, a piece of JSON Schema, to publish for a custom scalar |
gatool.inputs.send-explicit-nulls | Whether a null argument for a variable that declares a default reaches the GraphQL API as an explicit null, off by default. A null for a variable without a default is sent either way |
gatool.dev.experimental.generate-tools | Which tools to write from the schema: none by default, all-root-queries, all-root-queries-and-mutations or dynamic-three-step |
gatool.dev.experimental.generate-tools-for-deprecated-root-fields | Whether a deprecated root field becomes a tool, on by default. It applies only under all-root-queries and all-root-queries-and-mutations |
gatool.dev.experimental.generated-operations-include-deprecated-fields | Whether a deprecated field is selected, on by default. It applies only under all-root-queries and all-root-queries-and-mutations |
gatool.dev.experimental.generated-selection-depth | How many object levels a generated selection expands, 1 by default. Under those two modes the value is 1 to 5, and startup stops on any other value |
gatool.dev.experimental.dynamic-operations.corpus-format | How each schema coordinate is written for the search index, sdl by default. A schema coordinate is a type name, or a type and field name, such as Query.topRatedMovies. gloss is one sentence and raw the coordinate alone |
gatool.dev.experimental.dynamic-operations.search-backend | What ranks a search, bm25 by default. embedding uses the application's EmbeddingModel bean, and startup stops without one |
gatool.dev.experimental.dynamic-operations.search-token-budget | Tokens of search results one searchSchema call returns, 2000 by default. A token is counted as four characters of the text and the hint. A best match above the budget is named with its cost, so the model can read it with introspectType |
gatool.dev.experimental.dynamic-operations.ranked-hits | Coordinates a search ranks before the budget cuts the list, 50 by default |
gatool.dev.experimental.dynamic-operations.allow-mutations | Whether a mutation the model wrote runs, off by default. While off, the mutation root and the types only it reaches stay out of searchSchema, introspectType and the hints |
gatool.dev.experimental.dynamic-operations.include-deprecated-fields | Whether a deprecated field, argument, enum value or input field is searchable, readable and used as a step in a hint, off by default. While on, each is written with the reason the schema gives |
gatool.dev.experimental.dynamic-operations.validate-only | Whether executeGraphql returns the validated operation for a person to run, without calling the API, off by default |
gatool.dev.experimental.dynamic-operations.search-schema-description, .introspect-type-description and .execute-graphql-description | The whole description of one of the three dynamic tools. It replaces the text GATool writes, the sentences GATool generates from the schema and the settings included. Unset by default, and a blank value keeps GATool's text |
gatool.dev.experimental.dynamic-operations.max-depth | Deepest field nesting executeGraphql sends, 15 by default. 0 switches the check off |
gatool.dev.experimental.dynamic-operations.max-fields | Most field selections executeGraphql sends, spreads counted where spread, 500 by default. 0 switches the check off, and graphql-java's own ceiling of 100,000 selections still applies |
gatool.dev.experimental.dynamic-operations.max-aliases | Most aliased fields executeGraphql sends, 30 by default. 0 switches the check off |
gatool.dev.experimental.dynamic-operations.question-prefix | Text put in front of a question before it is embedded, empty by default. It is joined as it is, so quote the value in YAML to keep a trailing space |
gatool.dev.experimental.dynamic-operations.field-prefix | Text put in front of each schema field before it is embedded, empty by default, joined the same way |
gatool.dev.experimental.dynamic-operations.embed-batch-size | Schema fields sent to the embedding model in one call, 128 by default |
gatool.dev.experimental.dynamic-operations.vector-cache-directory | Where embedded fields are cached, so a schema is embedded once. The default is vectors inside gatool-<account> under the JVM's temporary directory (java.io.tmpdir), checked the way the schema cache's default folder is. A path of your own keeps the vectors on a persistent volume and is used as it is. A blank value embeds at every startup. The files are written through a temporary file and an atomic rename. A cache an earlier build wrote is embedded once more, because the key format changed |
gatool.dev.experimental.dynamic-operations.required-scopes | The scopes a caller needs for the dynamic tools on top of the baseline, all of them. Required under a shared credential, which is client credentials or a static header: one identity for every caller |
gatool.observations.include-content | Whether arguments and results join the gatool.call observation as high-cardinality values, off by default |
Every property has Javadoc, which an IDE shows while you type. The Javadoc says what the property does. The text below says what a setting costs.
A call to a remote API is bounded by spring.http.clients.connect-timeout and
spring.http.clients.read-timeout. Spring Boot's RestClient builder carries
both into the client GATool uses. GATool fills whichever of the two the
application left unset: 3 seconds to connect, 15 seconds for an answer. It says
so at INFO. Without a deadline, a call to an API that stops answering would hold
the thread until the socket closes. Under client-credentials and
token-exchange, the request to the issuer's token endpoint waits under the
same two deadlines. The API may take longer than the read timeout to answer a
query. The timeout can hit before the status arrives or inside the body. The
model then reads what happened, the deadline, and the property that sets it. It
is asked for fewer fields or a smaller page, because the same call is likely to
take as long again. When the connection did not open, the model reads that the
call did not run and that a later call may succeed. For any other transport
failure, the model reads that GATool cannot tell whether the call ran. A
mutation reads the same account of how far the call got. After a mutation fails
in any of the three ways, the model is asked to read the current state before
sending it again. The model's sentence and the operator's line open with the
same words. The operator reads a WARN line for each such call, query or
mutation. The line says how far the call got and ends with one of three causes:
-
the tool
could not reach the GraphQL API -
the API
took longer to answer than the read timeout of 15 seconds -
the tool
failed in transport while calling the GraphQL API, and the cause does not say whether the call ran
The stack is written at DEBUG. In embedded mode, where the GraphQL API runs inside the same application through Spring for GraphQL, the call stays in the same JVM. This release leaves that call without a deadline.
The MCP Java SDK client and Spring AI's MCP client wait 20 seconds for a tool
call by default. Their clock starts before GATool's does. Keep
spring.http.clients.read-timeout below the request timeout of the client
that calls the server, or raise spring.ai.mcp.client.request-timeout. A
client that reaches its own deadline first gives up on the call. The model
then reads the client's timeout message. GATool's sentence does not reach it.
After a mutation, GATool's sentence is the one that asks the model to read the
current state before it sends the write again.
GATool builds the request factory of that client from the application's
ClientHttpRequestFactoryBuilder bean and spring.http.clients.*. Startup
names the builder at INFO. The headers and interceptors a RestClientCustomizer
sets reach every request GATool sends. Startup names the customizer beans. A
request factory the customizer installs stays on the application's own clients.
GATool keeps its own factory, because that factory carries the two response
caps, gatool.api.max-response-size, and leaves redirects unfollowed. Without a
builder bean, ClientHttpRequestFactoryBuilder.detect() prefers Apache
HttpClient 5 and Jetty over the JDK client. httpclient5 on the classpath, for
any reason, moves GATool onto Apache's pool: 5 connections per route and 25 in
all. We measured about 48 calls per second on that pool, against 398 with the
JDK client. Startup warns on that builder.
spring.http.clients.imperative.factory=jdk selects the JDK client. A
ClientHttpRequestFactoryBuilder bean built from
ClientHttpRequestFactoryBuilder.httpComponents().withConnectionManagerCustomizer(...)
raises both sizes.
A proxy reaches GATool's client through that same builder bean. The bean
replaces the builder Spring Boot declares, so every client the application
builds from Spring Boot's builder goes through the proxy as well. A factory a
RestClientCustomizer installs stays on the application's own clients. A proxy
set only there does not reach GATool's calls:
@Bean
ClientHttpRequestFactoryBuilder<?> gatoolRequestFactoryBuilder() {
return ClientHttpRequestFactoryBuilder.jdk()
.withProxySelector(ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080)));
}
Mutual TLS reaches GATool's client the same way, through
spring.http.clients.ssl.bundle. GATool reads that property with the rest of
spring.http.clients.*. A RestClientCustomizer's factory stays on the
application's own clients here as well:
spring:
ssl:
bundle:
jks:
mtls:
keystore:
location: classpath:client-keystore.p12
password: ${KEYSTORE_PASSWORD}
truststore:
location: classpath:client-truststore.p12
password: ${TRUSTSTORE_PASSWORD}
http:
clients:
ssl:
bundle: mtls
In embedded mode, the
document runs on the application's WebGraphQlHandler bean. Without that bean,
GATool builds one from the ExecutionGraphQlService and the application's
WebGraphQlInterceptor beans. The interceptors therefore apply in every
application type, a non-web one included. Startup names them. That call
bypasses the servlet filter chain and every URL rule on
spring.graphql.http.path. Three controls remain on it: the MCP endpoint's
scopes, those interceptor beans, and method security in the resolvers.
Every MCP tool call is a Micrometer observation named gatool.call. Its
low-cardinality tags are gatool.call.name, gatool.operation.type (query
or mutation) and gatool.call.outcome. The outcome is one of success,
graphql-errors, api-refused, credential-unavailable,
response-too-large, result-too-large, tool-error, rate-limited,
insufficient-scope and off-request-thread. An answer the operator has to
act on reaches the log at WARN. A 5xx, and a 2xx without a GraphQL
response, are written once per tool per minute. The next such line says how
many answers were left out since the last one. A refused credential and a
redirect are written on every call. A 4xx stays at DEBUG, because the model
reads it and can act on it. The model corrects the call. After a 408, a 425
or a 429, it reads that a later call may succeed and sends the same call
again. It reads the same for a 5xx.
Four Spring AI settings take a default from GATool while the application leaves
them unset. The application's own value wins. spring.ai.mcp.server.protocol
becomes STATELESS over HTTP. spring.ai.mcp.server.name becomes
${spring.application.name:mcp-server}, so an application without a name is
called mcp-server. spring.ai.mcp.server.version becomes
spring.application.version when that property is set.
spring.ai.mcp.server.instructions becomes one sentence that says what every
tool result holds. Under dynamic-three-step, it describes the loop the
dynamic tools serve: search, read, run and correct.
spring.main.lazy-initialization=true is supported. GATool keeps Spring AI's
server bean eager, so the endpoint answers from the first request. It keeps its
tool catalog eager on both starters, so a broken operation file still stops
startup.
Spring AI's line about tool methods. An application whose tools all come
from operation files reads this line at every startup, at WARN. It comes from
SyncStatelessMcpToolProvider on the stateless transport and from
SyncMcpToolProvider on the stateful one:
No tool methods found in the provided tool objects: []
Spring AI's annotation scanner collects the beans that carry @McpTool
methods and builds tool specifications from them. Without such a method the
list of beans is empty, which is the [], and the provider says so. The tools
of the operation files are registered another way. GATool publishes them as a
list of tool specifications of its own, and Spring AI's server reads that list
beside the scanner's. Its line Registered tools: follows at INFO with the
count, which includes them, and tools/list serves them. An application
without @McpTool methods of its own can set
spring.ai.mcp.server.annotation-scanner.enabled=false. The scanner and its
line then stay out of the startup.
A null argument is the one place GATool writes JSON with a mapper of its own.
GraphQL reads a variable set to null as clearing a value. It reads a variable
left out as a request for the default. That default comes from the operation,
or from the schema's argument or input field the variable fills. Every null
GATool holds therefore reaches the request it sends. An application that sets
spring.jackson.default-property-inclusion to a value dropping nulls would
otherwise lose them on the way out. Startup says so on the boot where that
happens. Every other HTTP client in the application writes with the mapper it
had.
A null argument reaches the API. Spring AI's MCP transports read each
request body with the mcpServerJsonMapper bean. Spring AI's own bean drops a
null map entry while reading. GATool declares that bean with a null map value
kept, so a model can clear a value by sending null. The rules above decide
what the API receives. The protocol JSON the transports write is unchanged. A
result map of your own tool that holds a null value writes it. Declare a bean
of that name yourself and GATool leaves it to you.
The unsafe switches
Five properties weaken a control the server would otherwise keep: four under
gatool.mcp.security.unsafe and one under gatool.api.credentials.unsafe. Each
is false by default. While one of the four is on, GATool logs a warning naming
it at every startup. The fifth,
gatool.api.credentials.unsafe.forward-client-tokens, warns at every startup on
which GATool creates its own strategy bean for it. An application with an
ApiCredentialStrategy bean of its own reads an INFO line naming that bean.
| Property | What you give up |
|---|---|
gatool.mcp.security.unsafe.allow-mcp-calls-without-authentication | The unauthenticated switch. Any caller that reaches the port may call every tool. Keep the port on a loopback address with server.address, or behind a gateway that authenticates for it. Startup warns while server.address is unset, because the server then listens on every interface. Spring Boot's own default stays as it is, the way Spring AI's MCP starter leaves it. |
gatool.api.credentials.unsafe.forward-client-tokens | The forwarded token. Every call to the GraphQL API carries the token the MCP caller sent. MCP says a server must not pass that token through. The API cannot tell GATool from the caller. A token minted for this server reaches another audience. A server with this switch on is outside the MCP specification. It only works with an API that skips audience validation. |
gatool.mcp.security.unsafe.request-every-scope-at-sign-in | The metadata and every 401 name every scope this server knows, so a client asks for all of them at sign-in. MCP's security best practices call that a common mistake. It serves clients whose step-up, a second sign-in that asks for more scopes, fails. |
gatool.mcp.security.unsafe.allow-unlimited-tool-calls | The rate limiter stops running. MCP requires a server to rate limit tool calls, so a deployment that sets this covers it at a gateway. |
gatool.mcp.security.unsafe.allow-superseded-mcp-revisions | The endpoint accepts 2024-11-05 and 2025-03-26. GATool serves 2025-03-26 in part, because it cannot read the JSON-RPC batches that revision asks for. |
gatool.dev.experimental.* is separate. Those settings leave every control in
place. They publish more of your API than a folder of trusted documents, the
operation files checked into your repository, would. They may change in any
release.