DEV Community

Solon Framework
Solon Framework

Posted on

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 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>
Enter fullscreen mode Exit fullscreen mode

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
  • $ref expansion and circular-ref markers
  • path placeholder replacement with URL encoding
  • response truncation (maxContextLength)
  • per-source allowedTools / disallowedTools
  • ApiAuthenticator (bearer / apiKey / custom)
  • skip @Deprecated operations
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();
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
  • removeMcpServer closes 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

  1. Unique tool/operation names across sources — stored lower-case, case-insensitive at call time.
  2. Path params must use {name} — gateway URL-encodes replacements; leftover {xxx} fails loudly.
  3. Deprecated OpenAPI ops are skipped automatically.
  4. Disabled sources (ApiSource.setEnabled(false) / provider setEnabled(false)) stay registered for management but leave the tool index until re-enabled + refresh.
  5. Permission edits need refresh — changing allow/deny on a client copy is not enough by itself.
  6. Thresholds are product decisions — lower dynamicThreshold earlier 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

Solon version used for this write-up: v4.0.3.

Top comments (3)

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

The staged discovery model is a useful answer to schema bloat, but I would add one more invariant: discovery must not widen authority. search_tools and get_tool_detail should 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 and call_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.

Collapse
 
marcusykim profile image
Marcus Kim

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.

Collapse
 
solonjava profile image
Solon Framework

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:

  1. start conservative (prefer LIST/SEARCH over FULL when the surface is large)
  2. on each ApiSource / MCP server, set allowedTools (empty = all) and disallowedTools (empty = none blocked)
  3. version those lists with the agent rollout, then call refreshApi / refreshMcpServer so only the filtered surface is re-indexed
  4. watch search → call success and task completion, not just token spend
  5. keep refresh non-disruptive (shadow swap) so in-flight turns never see an empty tool table

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.