Repository navigation
Description
Problem
xAI's API enforces a hard limit of 200 tools per request. When multiple MCP servers are configured, the total easily exceeds 200 and the API returns:
Bad Request: Maximum tools limit reached. 520 tools have been provided but the maximum is 200.
This is a server-side limit — @ai-sdk/xai and opencode pass all tools through without validation. There is no way to work around this from the user's opencode.json config today.
Other providers may have similar limits (e.g. some OpenAI-compatible endpoints).
Proposed Solution
Implement a hot/cold tool split inspired by Claude Code's deferred tools pattern:
- Detect provider tool limit — map known providers to their max (xAI/Grok → 200)
- Split tools — built-in tools get priority ("hot", full JSON schema sent to API), remaining MCP tools become "cold"
- System prompt injection — cold tool names + descriptions are appended to the system prompt so the LLM knows they exist
- Dispatcher tool — a single
call_mcp_tooltool is added to the hot set. The LLM calls it with{ tool_id, args }to invoke any cold tool at runtime
This keeps the API request under the limit while preserving access to all registered tools.
Implementation
Working proof-of-concept across 3 files:
packages/opencode/src/session/llm.ts — core logic:
const providerToolLimit = getProviderToolLimit(input.model.providerID)
let tools = allTools
if (providerToolLimit !== undefined && Object.keys(allTools).length > providerToolLimit) {
const entries = Object.entries(allTools)
const builtin = entries.filter(([k]) => !k.includes(":"))
const mcp = entries.filter(([k]) => k.includes(":"))
const hotSlots = providerToolLimit - 1 // reserve 1 for dispatcher
const hotEntries = [...builtin, ...mcp].slice(0, hotSlots)
const coldEntries = [...builtin, ...mcp].slice(hotSlots)
const coldMap = Object.fromEntries(coldEntries)
// Inject cold tool catalog into system prompt
messages.push({ role: "system", content: coldSection })
// Add dispatcher tool
tools = { ...Object.fromEntries(hotEntries), call_mcp_tool: dispatcher }
}packages/opencode/src/session/llm.ts — provider limit map:
function getProviderToolLimit(providerID: string): number | undefined {
const id = providerID.toLowerCase()
if (id.includes("xai") || id.includes("grok")) return 200
return undefined
}packages/opencode/src/config/config.ts — optional per-server tool whitelist (McpLocal + McpRemote):
tools: z
.array(z.string())
.optional()
.describe("Whitelist of tool names to expose from this MCP server. If omitted, all tools are exposed.")packages/opencode/src/mcp/index.ts — filter tools by whitelist before registration:
const filtered = mcp.tools?.length
? listed.filter((t) => mcp.tools.includes(t.name))
: listedFuture Improvements
- Make the limit configurable per provider in
opencode.json(e.g."toolLimit": 200) - Support
ToolSearch-style lazy schema loading — LLM requests a tool's full schema before calling it, instead of getting all schemas upfront - Allow user to prioritize which MCP tools should be "hot" vs "cold"
Environment
- opencode 1.3.9
- xAI provider (Grok models)
- Tested with 520+ tools, confirmed working after patch
Activity
github-actions commentedon Apr 1, 2026
This issue doesn't fully meet our contributing guidelines.
What needs to be fixed:
[FEATURE]:(current title usesfeat:)Please edit this issue to address the above within 2 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
If you believe this was flagged incorrectly, please let a maintainer know.
[-]feat: deferred tools for providers with tool count limits (xAI 200 max)[/-][+][FEATURE]: deferred tools for providers with tool count limits (xAI 200 max)[/+]github-actions commentedon Apr 1, 2026
Thanks for updating the issue! It now meets our contributing guidelines. The
[FEATURE]:title prefix is in place and the description clearly explains the problem, proposed solution, and environment. Theneeds:compliancelabel has been removed.github-actions commentedon Jul 4, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.