Skip to content

Feature Request: Two-Step MCP Tool Discovery for On-Demand Loading #16206

@sanathusk

Description

@sanathusk

Feature Request: Two-Step MCP Tool Discovery for On-Demand Loading

Summary

Implement a two-step MCP tool discovery mechanism where tool definitions are loaded lazily on-demand rather than eagerly fetching all tools from all connected MCP servers on every LLM call.


Current Implementation

Tool Resolution Flow

  1. MCP Client Connection (packages/opencode/src/mcp/index.ts:183)

    • When a project loads, MCP clients are created via create() during Instance.state() initialization
    • All configured MCP servers with enabled !== false attempt to connect
  2. Tool Definition Fetching (packages/opencode/src/mcp/index.ts:566-606)

    • MCP.tools() is called during every LLM call via resolveTools()
    • For each connected MCP client, it calls client.listTools() to fetch ALL tool definitions
    • No caching mechanism exists - tools are fetched fresh on every call
    // mcp/index.ts:578-604
    const toolsResults = await Promise.all(
      connectedClients.map(async ([clientName, client]) => {
        const toolsResult = await client.listTools()  // Fetches ALL tools
        // ...
        for (const mcpTool of toolsResult.tools) {
          result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(...)
        }
      }),
    )
  3. Tools Passed to LLM (packages/opencode/src/session/llm.ts:207-208)

    • ALL built-in tools + ALL MCP tools are passed to the model
    • No filtering or selection mechanism exists
    // llm.ts:207-208
    activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
    tools,

Key Files

  • packages/opencode/src/mcp/index.ts - MCP client management and tool fetching
  • packages/opencode/src/session/prompt.ts:730-909 - resolveTools() function
  • packages/opencode/src/session/llm.ts:154-208 - Tool resolution and passing to LLM

The Problem

  1. Context Bloat: Every LLM call includes ALL tool definitions from ALL connected MCP servers, even if only a small subset is relevant to the current task.

  2. No Lazy Loading: Tools are fetched eagerly - there's no mechanism to defer loading tool definitions until they're actually needed.

  3. Performance Overhead: Each LLM call triggers listTools() for every connected MCP server, even if the tools won't be used.

  4. Scalability Issues: As users add more MCP servers with many tools, the context size grows linearly, potentially hitting model token limits.


Proposed Solution: Two-Step Tool Discovery

Step 1: Tool Listing (Metadata Only)

When resolving tools for an LLM call:

  1. First, fetch only the tool names and descriptions from MCP servers (not full schemas)
  2. Store this metadata in a cache with TTL
  3. Pass only tool names/descriptions to the model (lightweight)

This step would use MCP's existing tools/list method but would cache results.

Step 2: On-Demand Tool Loading

  1. The model decides which tools to use based on the lightweight metadata
  2. When a tool is actually invoked, fetch its full definition (input schema, description)
  3. Cache the full definition for subsequent calls

Alternatively, implement a tool selection mechanism where:

  1. User/agent explicitly specifies which MCP servers to enable per task
  2. Or context-aware filtering based on task keywords

Implementation Approach

Option A: Cached Tool Listings

// Pseudocode
async function resolveTools() {
  // Step 1: Get lightweight tool listings (cached)
  const toolMeta = await MCP.toolMetadata()  // Only names + descriptions

  // Step 2: Pass metadata to model
  // Model selects tools...

  // Step 3: On actual invocation, load full definition
  const fullTool = await MCP.getToolDefinition(toolName)
}

Option B: Explicit Tool Selection

Add configuration to enable/disable MCP servers per context:

// In opencode.json or session config
{
  "mcp": {
    "server1": { "type": "remote", "url": "...", "enabled": false },
    "server2": { "type": "remote", "url": "...", "enabled": true }
  }
}

// Or dynamically in session
session.mcpServers = ["server2"]  // Only load tools from server2

Option C: Context-Aware Filtering

Implement intelligent filtering based on:

  • Task description/keywords
  • File types being edited
  • Recent tool usage patterns

Benefits

  1. Reduced Context Size: Only relevant tool definitions are loaded
  2. Faster LLM Calls: Less data to serialize and send to model
  3. Better Scalability: Users can connect more MCP servers without performance degradation
  4. Token Savings: More room for actual code/context in prompts

Backward Compatibility

  • Default to current behavior (load all tools) for existing configurations
  • New behavior opt-in via config flag: experimental.mcp_lazy_loading: true

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions