Skip to main content

Configuration

PropertyWhat it does
gatool.api.urlThe 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.locationThe 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-nameThe header that carries a registry key when the location is an http or https URL
gatool.api.schema.header-valueIts value, kept in the environment
gatool.api.schema.cache-directoryWhere 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.strategyHow 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-nameThe header that carries the credential, such as Authorization or X-API-Key
gatool.api.credentials.header-valueIts value, such as a bearer token, kept in the environment
gatool.api.credentials.client-registration-idThe Spring Boot client registration the two OAuth2 strategies authenticate through
gatool.api.credentials.audienceThe audience token exchange asks the issuer for, which the API validates
gatool.api.credentials.resourceThe resource token exchange names to the issuer, sent when set
gatool.mcp.operations.locationsWhere 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.locationsWhere 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.strategycamel-case, snake-case or as-written
gatool.results.max-charactersLargest 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-schemaWhether 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-successWhether 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-charactersCharacters 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-tokensTokens 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-tokensWhitespace 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-sizeLargest 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-sizeLargest 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-minuteCalls 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-pairsMost 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-sizeLargest request body the MCP endpoint reads, 256KB by default
gatool.mcp.transport.allowed-originsThe 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-countMost stateful sessions the server keeps at once, 1000 by default
gatool.mcp.sessions.max-count-per-callerMost stateful sessions one caller may hold at once, 100 by default. Zero or below turns it off
gatool.mcp.sessions.idle-timeoutHow 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-scopesScopes every MCP call needs, published in the metadata and every 401
gatool.mcp.security.resourceThe canonical resource URI the metadata publishes. Unset publishes the request's URL
gatool.mcp.stdio.granted-scopesThe scopes the process that started a stdio server holds. Tools requiring scopes are checked against them
gatool.mcp.stdio.log-to-stderrWhether 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-nullsWhether 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-toolsWhich 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-fieldsWhether 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-fieldsWhether 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-depthHow 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-formatHow 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-backendWhat 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-budgetTokens 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-hitsCoordinates a search ranks before the budget cuts the list, 50 by default
gatool.dev.experimental.dynamic-operations.allow-mutationsWhether 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-fieldsWhether 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-onlyWhether 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-descriptionThe 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-depthDeepest field nesting executeGraphql sends, 15 by default. 0 switches the check off
gatool.dev.experimental.dynamic-operations.max-fieldsMost 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-aliasesMost aliased fields executeGraphql sends, 30 by default. 0 switches the check off
gatool.dev.experimental.dynamic-operations.question-prefixText 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-prefixText 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-sizeSchema fields sent to the embedding model in one call, 128 by default
gatool.dev.experimental.dynamic-operations.vector-cache-directoryWhere 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-scopesThe 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-contentWhether 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.

PropertyWhat you give up
gatool.mcp.security.unsafe.allow-mcp-calls-without-authenticationThe 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-tokensThe 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-inThe 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-callsThe 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-revisionsThe 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.