Skip to content

[FEATURE]: Client-side file reference resolution for MCP tool arguments #23467

Description

@hchangjae
  • I have verified this feature I'm about to request hasn't been suggested before

Problem

When an AI agent calls an MCP tool with a large payload (e.g., updating a Confluence page with 300KB+ ADF JSON), the agent must emit the entire payload as output tokens. This hits the model's max_output_tokens limit, causing:

  1. Truncated tool calls — the JSON argument gets cut mid-stream (related: fix(opencode): recover from truncated tool calls instead of failing silently #21688)
  2. Multi-turn workarounds — agent writes chunks across multiple turns, wasting context and time
  3. Silent failures — truncated arguments parse as invalid, tool call fails

Quantified impact:

  • A single Confluence page update: ~160K tokens as tool argument
  • Anthropic Claude: 32K max output tokens (Sonnet), 64K (Opus)
  • Result: Any MCP tool call with >32K token payload is physically impossible in a single turn

This is the output-side counterpart to the input bloat that experimental.mcp_lazy (#8771) solved. Lazy-load reduced system prompt tokens; this proposal reduces output tokens.

Proposed Solution

Allow the agent to reference a local file instead of inlining the full payload. The client resolves the reference before sending to the MCP server.

Agent outputs (~50 bytes instead of 160K tokens):

{
  "tool": "confluence_update_page",
  "arguments": {
    "page_id": "123456",
    "content": { "$file": "/tmp/adf-payload.json" }
  }
}

Client intercepts and resolves:

function resolveFileRefs(args: Record<string, unknown>): Record<string, unknown> {
  for (const [key, value] of Object.entries(args)) {
    if (isFileRef(value)) {
      args[key] = fs.readFileSync(value.$file, 'utf-8');
    }
  }
  return args;
}

function isFileRef(v: unknown): v is { $file: string } {
  return typeof v === 'object' && v !== null && '$file' in v && typeof v.$file === 'string';
}

Workflow:

  1. Agent writes large content to a temp file using existing Write/Bash tools
  2. Agent calls MCP tool with {"$file": "/tmp/payload.json"} as argument
  3. Client detects $file reference, reads file, injects content into argument
  4. MCP server receives the full payload as normal — no protocol change needed

Configuration (opt-in):

// opencode.json
{
  "experimental": {
    "mcp_file_refs": true  // default: false
  }
}

Why This Matters

  • Unblocks cloud MCP servers — Confluence, Notion, Google Docs integrations with large documents become viable
  • Zero protocol change — purely client-side; MCP servers see normal arguments
  • Complements lazy-load — input optimization (mcp_lazy) + output optimization (mcp_file_refs) = full token efficiency
  • Industry precedent — Claude Code community validated this pattern via PreToolUse hooks (anthropics/claude-code#45770); MCP spec has SEP-2356 (file input for tools) in draft

Security Considerations

  • Only resolve files under allowed paths (e.g., temp dirs, workspace)
  • Reject absolute paths outside workspace unless explicitly allowed
  • Log file reference resolutions for auditability

Metadata

Metadata

Assignees

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