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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions