Skip to content

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

@hchangjae

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
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