When an agent can see 80 tools at once, the model does not get smarter. It gets noisier. Token bills climb, tool choice drifts, and a simple “check order status” prompt suddenly carries half of your company’s OpenAPI surface.
Solon AI (v4.0.3) answers that with the Gateway Talent trio in solon-ai-talent-gateway:
| Talent | Source of tools | Unit of management |
|---|---|---|
OpenApiGatewayTalent |
OpenAPI / Swagger docs | one API source |
ToolGatewayTalent |
local / MCP FunctionTools |
one tool, runtime add/remove |
McpGatewayTalent |
MCP client connections | one MCP server, plus allow/deny lists |
All three share the same four-stage adaptive discovery model. Small catalogs stay fully expanded. Large catalogs fold into summary → name list → search, so the LLM only pays for what it needs.
This article sticks to official APIs from article/1353, article/1389, article/1335, and article/1293.
Why a gateway is a Talent, not “just more tools”
In Solon AI, a Tool is an atomic function. A Talent packages tools with instruction, activation, and SOP-style constraints.
Gateways need that packaging:
- too many raw schemas blow up context
- tools from different systems need grouping and lifecycle
- the model should discover capability step by step, not swallow everything on turn 1
That is why you hang gateways with defaultTalentAdd(...) (or request-scoped talentAdd), not by dumping every OpenAPI operation into defaultToolAdd.
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-ai-talent-gateway</artifactId>
</dependency>
MCP features also need solon-ai-mcp. OpenAPI parsing pulls in swagger-parser (v2 and v3); exclude it only if you never load docs.
The four stages (shared by all three gateways)
| Stage | Trigger (defaults) | What the model sees | Proxy tools exposed |
|---|---|---|---|
| FULL | count <= dynamicThreshold (8) |
full tool schemas | original tools (OpenAPI collapses to call_api) |
| SUMMARY |
8 < count <= listThreshold (30 OpenAPI / 40 others) |
name + description (+ endpoint) |
get_*_detail + call_*
|
| LIST |
listThreshold < count <= searchThreshold (100) |
grouped names only |
search_* + get_*_detail + call_*
|
| SEARCH | count > 100
|
no catalog dump | forced search path: search_* → detail → call |
Design rule from the docs: when the catalog is small, embed full schemas in the instruction so the model can call in one step; when it grows, fold information and force progressive discovery.
Shared knobs:
| Method | Default | Notes |
|---|---|---|
dynamicThreshold(n) |
8 | FULL ceiling |
listThreshold(n) |
30 (OpenAPI) / 40 (others) | SUMMARY ceiling |
searchThreshold(n) |
100 | LIST ceiling |
retryConfig(maxRetries) |
3 | shared |
retryConfig(maxRetries, retryDelayMs) |
3, 1000 | McpGatewayTalent delay form |
maxContextLength(n) |
8000 | OpenApiGatewayTalent response truncate |
1. OpenApiGatewayTalent — turn Swagger into an agent surface
Use this when tools already exist as OpenAPI / Swagger documents (remote http:// or local classpath:).
Capabilities called out officially:
- Swagger 2.0 + OpenAPI 3.0 auto detection
- multi-source load with tag-based grouping
-
$refexpansion and circular-ref markers - path placeholder replacement with URL encoding
- response truncation (
maxContextLength) - per-source
allowedTools/disallowedTools -
ApiAuthenticator(bearer / apiKey / custom) - skip
@Deprecatedoperations
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.talents.gateway.OpenApiGatewayTalent;
import org.noear.solon.ai.talents.gateway.openapi.ApiAuthenticator;
import org.noear.solon.ai.talents.gateway.openapi.ApiSource;
import java.time.Duration;
import java.util.Arrays;
OpenApiGatewayTalent apiTalent = new OpenApiGatewayTalent()
.dynamicThreshold(5)
.listThreshold(30)
.searchThreshold(100)
.maxContextLength(10_000)
.defaultTimeout(Duration.ofSeconds(30))
.defaultAuthenticator(ApiAuthenticator.bearer("your-token"));
// simple multi-source mount
apiTalent.addApi("http://order-service:8081/v3/api-docs", "http://order-service:8081");
apiTalent.addApi("classpath:swagger/user-api.json", "http://user-service:8082");
// fine-grained source
ApiSource pay = new ApiSource();
pay.setDocUrl("http://pay-service:8083/v3/api-docs");
pay.setApiBaseUrl("http://pay-service:8083");
pay.setAllowedTools(Arrays.asList("createPayment", "queryPayment"));
pay.setAuthenticator(ApiAuthenticator.bearer("pay-service-token"));
pay.setTimeout(Duration.ofSeconds(60));
apiTalent.addApi(pay);
ReActAgent agent = ReActAgent.of(chatModel)
.role("Senior business assistant")
.instruction("Prefer the provided business APIs. Do not invent path or body fields.")
.defaultTalentAdd(apiTalent)
.build();
// runtime lifecycle
apiTalent.removeApi("http://order-service:8081/v3/api-docs");
apiTalent.refreshApi("http://pay-service:8083/v3/api-docs");
apiTalent.refreshApiAll();
Built-in OpenAPI proxy tools by stage:
| Tool | Modes | Role |
|---|---|---|
call_api |
all | execute REST call (path/query/body/multipart) |
get_api_detail |
SUMMARY / LIST / SEARCH | full parameter schema |
search_apis |
LIST / SEARCH | multi-keyword AND search |
Auth priority: source authenticator > defaultAuthenticator.
Runtime permission edits happen on the ApiSourceClient copy, then refreshApi(...):
var client = apiTalent.getApiSource("http://pay-service:8083/v3/api-docs");
client.setAllowedTools(Arrays.asList("createPayment"));
client.getDisallowedTools().add("queryRefund");
apiTalent.refreshApi("http://pay-service:8083/v3/api-docs");
2. ToolGatewayTalent — govern local (and mixed) FunctionTools
Use this when tools live in code — AbsToolProvider, single FunctionTool, or already-fetched MCP tools — and you need runtime, imperative add/remove at tool granularity.
import org.noear.solon.ai.talents.gateway.ToolGatewayTalent;
ToolGatewayTalent toolGateway = new ToolGatewayTalent()
.dynamicThreshold(8)
.listThreshold(40)
.searchThreshold(100);
toolGateway.addTool("Finance", financeToolProvider);
toolGateway.addTool("HR", hrToolProvider);
toolGateway.addTool("General", singleFunctionTool);
ReActAgent agent = ReActAgent.of(chatModel)
.role("Enterprise ops assistant")
.defaultTalentAdd(toolGateway)
.build();
Built-in proxy tools:
| Tool | Modes | Role |
|---|---|---|
call_tool |
SUMMARY / LIST / SEARCH | proxy execute by tool_name + tool_args
|
get_tool_detail |
SUMMARY / LIST / SEARCH | full JSON Schema |
search_tools |
LIST / SEARCH | keyword search |
Important FULL-mode note from the docs: original business tools are exposed directly to the LLM; the proxy trio appears only after the catalog leaves FULL.
3. McpGatewayTalent — many MCP servers, one agent
Use this when tools arrive as MCP services and you prefer managing connections (with optional allow/deny lists) over hand-picking every tool at runtime.
import org.noear.solon.ai.mcp.client.McpServerParameters;
import org.noear.solon.ai.talents.gateway.McpGatewayTalent;
import java.util.Arrays;
McpGatewayTalent mcpGateway = new McpGatewayTalent()
.dynamicThreshold(8)
.listThreshold(40)
.searchThreshold(100)
.retryConfig(3, 1000);
mcpGateway.addMcpServer("weather", new McpServerParameters().then(e -> {
e.setTransport("stdio");
e.setCommand("npx");
e.addArgVar("-y");
e.addArgVar("@modelcontextprotocol/server-weather");
}));
mcpGateway.addMcpServer("database", new McpServerParameters().then(e -> {
e.setTransport("stdio");
e.setCommand("npx");
e.setArgs(Arrays.asList("-y", "@modelcontextprotocol/server-database"));
e.setAllowedTools(Arrays.asList("query", "list_tables"));
e.setDisallowedTools(Arrays.asList("drop_table", "execute_raw"));
}));
// alternative: pass an existing McpClientProvider
// mcpGateway.addMcpServer("database", provider);
ReActAgent agent = ReActAgent.of(chatModel)
.role("Ops agent with remote MCP tools")
.defaultTalentAdd(mcpGateway)
.build();
mcpGateway.removeMcpServer("weather"); // remove + close connection
mcpGateway.refreshMcpServer("database"); // after allow/deny changes
mcpGateway.refreshMcpServerAll();
Official lifecycle details worth keeping:
- refresh uses a shadow swap (add new tools, then remove old ones) so in-flight calls do not see a blank tool table
-
removeMcpServercloses the underlying connection - disable via
McpClientProvider.setEnabled(false)only drops the tool index; the provider stays alive until re-enabled + refresh
Proxy tool names match ToolGatewayTalent: call_tool / get_tool_detail / search_tools.
Choosing the right gateway (and the common MCP confusion)
Pick by ingress shape and mutation unit:
| If your tools… | Prefer |
|---|---|
| are OpenAPI / Swagger documents | OpenApiGatewayTalent |
are code-side FunctionTools you add/remove at runtime |
ToolGatewayTalent |
| come from MCP servers you want to attach as wholes | McpGatewayTalent |
Common mistake: “I need per-tool MCP control, so I must use ToolGatewayTalent.”
Not required. McpGatewayTalent already supports tool-level exposure through allowedTools / disallowedTools on McpServerParameters. After changing lists, call refreshMcpServer(...).
Real split:
- ToolGatewayTalent = runtime, imperative, single-tool mutations
- McpGatewayTalent = connection lifecycle + declarative allow/deny at attach/refresh time
You can also hang more than one gateway Talent on the same agent when sources differ.
Production checklist from the official notes
- Unique tool/operation names across sources — stored lower-case, case-insensitive at call time.
-
Path params must use
{name}— gateway URL-encodes replacements; leftover{xxx}fails loudly. - Deprecated OpenAPI ops are skipped automatically.
-
Disabled sources (
ApiSource.setEnabled(false)/ providersetEnabled(false)) stay registered for management but leave the tool index until re-enabled + refresh. - Permission edits need refresh — changing allow/deny on a client copy is not enough by itself.
-
Thresholds are product decisions — lower
dynamicThresholdearlier if FULL schemas already dominate context; raise it only when the catalog is tiny and high-precision one-shot calls matter.
Minimal “when do I leave plain tools?” rule
Stay on AbsToolProvider + @ToolMapping + defaultToolAdd while the set is small and always relevant.
Move to a Gateway Talent when any of these becomes true:
- dozens of OpenAPI operations from multiple services
- MCP servers that should not dump full schemas every turn
- runtime plugin-style tool catalogs that grow and shrink
- you need progressive discovery instead of “show everything”
Gateways do not replace ReAct planning, HITL, or session memory. They solve a narrower, painful problem: keep tool surfaces usable as they scale.
Official references
- Gateway trio + four stages: https://solon.noear.org/article/1353
- Maven module
solon-ai-talent-gateway: https://solon.noear.org/article/1389 - Tool vs Talent: https://solon.noear.org/article/1335
- ReActAgent config /
talentsfield: https://solon.noear.org/article/1293 - ReAct call options: https://solon.noear.org/article/1347
- MCP client basics: https://solon.noear.org/article/993
Solon version used for this write-up: v4.0.3.
Top comments (3)
The staged discovery model is a useful answer to schema bloat, but I would add one more invariant: discovery must not widen authority.
search_toolsandget_tool_detailshould return only tools the current user, tenant, and agent policy could actually call, using the same policy snapshot as execution. Otherwise the catalog becomes a side channel and a race is possible when allow/deny lists change between discovery andcall_tool. A useful test is to start a turn, refresh the server policy mid-turn, then verify that the final call either uses the pinned capability version or fails closed with a structured policy-changed error. That would complement the shadow swap nicely: availability stays continuous, while authorization remains explicit and reviewable.The 80-tool example makes the core problem obvious: context blow-up is a product failure, not just a token-cost problem. The four-stage FULL SUMMARY LIST SEARCH progression, plus the shadow swap during MCP refreshes, gives teams two useful guarantees at once: the model gets less information when it needs less, and in-flight calls do not encounter an empty tool table. For a founder or platform engineer, I'd treat those thresholds and allow/deny lists as part of the operating contract-version them, observe search-to-call success, and tune them against real task completion, because a smaller surface is only better if discovery does not become.
Thanks, Marcus — this is exactly how we hope teams use Gateway Talents.
Treating thresholds and allow/deny lists as an operating contract is the right move. In Solon that contract is also per source, not only global:
A smaller surface only helps if discovery still works. If search quality drops, raise the stage or tighten the allow-list before blaming the model.
Appreciate the founder/platform-engineer framing.