Gateway Talents in Solon AI: OpenAPI, Tool, and MCP Surfaces That Scale Without Context Blow-Up
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 sur
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.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.